kym8821 님의 블로그
Spring Boot에서 테스트코드 작성하기 본문
테스트 코드 작성과 중요성
테스트코드의 중요성과 단점
테스트 코드 작성의 장점
- 사전 테스트를 통한 소프트웨어 안정성 보장
- 사전 테스트를 통한 빠른 버그 발견
- 테스트 코드를 통해 코드의 동작 방식을 검증할 수 있음
- 코드에 변경사항이 있을 때, 이를 검증할 수 있기에 유지보수에 용이함
- 코드의 동작을 정의하기에 협업에서 발생하는 문제를 줄여
테스트 코드 작성의 단점
- 따로 테스트 코드를 작성해야 하므로 개발 시간이 늘어난다.
- 코드가 변경되었을 때 테스트코드도 함께 수정해야하므로 유지보수 시간이 늘어난다.
- 테스트 코드 작성에 과도한 시간을 투자하면, 개발 생산성이 저하될 수 있다.
- 테스트 코드 작성 경험과 도구 학습을 위한 시간을 투자해야 한다.
즉, 테스트코드는 코드의 안정성과 버그 방지 등의 장점이 있지만, 추가적인 코드를 작성 및 유지보수해야 하기에 많은 자원이 필요한 작업이기도 하다.
TDD VS 단위 테스트 VS 통합 테스트
단위 테스트
단위 테스트란, 하나의 모듈에 대해서 독립적으로 진행되는 가장 작은 단위의 테스트를 말한다.
일반적으로 각 모듈 하나의 클래스 또는 메서드 수준으로 정해진다.
통합 테스트
통합 테스트란, 여러 모듈들이 함께 잘 작동하는지 확인하기 위한 테스트이다.
통신, DB 등 단위 테스트로 확인하기 어려운 부분까지 통합하여 테스트할 수 있기에 용이하다.
단위 테스트와 달리, 여러 모듈이 테스트에 참여하기에 어떤 모듈에서 문제가 발생하는지 파악하기 어렵다.
TDD (테스트 주도 개발)
TDD는 테스트 코드를 우선으로 작성해고, 이를 통과하는 코드를 작성하는 것을 반복하는 소프트웨어 방법론이다.
TDD는 Red-Green-Blue 라는 3단계 개발주기를 갖는다.
- Red : 실패하기 위한 테스트 코드 작성
- Green : 테스트 코드를 성공하기 위한 코드 작성
- Blue : 중복 제거 등의 코드 리펙토링
TDD는 코드 개발 이전에 테스트 케이스를 작성하고, 테스트 코드 작성 중 발생하는 예외들도 테스트 케이스에 포함한다.
Given - When - Then 패턴
Given - When - Then 패턴은 테스트 코드 작성 시 자주 사용되는 패턴이다.
해당 패턴은 테스트 주도 개발에 자주 사용된다.
현재 연도를 받아오는 아래 코드를 검증하는 과정을 given-when-then 세 단계로 나누어보자.
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Enumeration;
class prog{
public static void main(String[] args){
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime now = ZonedDateTime.now(zoneId);
if(now.getYear()==2025) System.out.println("success");
else System.out.println("fail");
}
}
이 코드는 기준 시간대(ZoneId)를 준비하고, 해당 시간대의 현재 연도가 2025인지를 검증하고 있다.
Given
given 단계는 테스트 실행을 준비하는 단계이다.
위 코드에서는 기준 시간대의 연도를 받아오기 위해서는 기준 시간대를 정할 필요가 있다.
따라서 기준 시간대를 받아오는 과정이 given단계이다.
When
when 단계는 테스트를 진행하는 단계이다.
위 코드에서는 기준 시간대의 시간을 받아오는 것을 통해 실제 시간을 저장한다.
따라서 now라는 객체에 현재 시간을 저장하는 것이 when 단계이다.
Then
then 단계는 테스트 결과를 검증하는 단계이다.
위 코드에서는 now에 저장한 시간의 연도가 2025가 맞는지 조건문으로 검증한다.
따라서 now의 연도를 검증하는 것이 then 단계이다.
지금은 given-when-then에 대해서 이해만 하고 있으면 크게 문제될 것은 없다.
모킹(Mocking)을 활용한 단위 테스트
모킹 (Mocking) 과 사용처
모킹이란, 테스트하고자 하는 모듈이 사용하는 외부 의존성들을 제거하기 위해 사용하는 개념이다.
이를 위해서 실제 객체 대신 가짜 객체(Mock)을 사용한다.
예를 들어서, DB 접근을 필요로 하는 서비스 코드가 있다면 Mock 객체를 사용하여 DB에 접근한 셈 치는 것으로 테스트를 진행할 수 있다.
실제 DB 관련 기능들이 정상 작동하는지는 다른 테스트에서 진행하면 될 일이다.
모킹의 사용처
모킹은 주로 단위 테스트에서 유용하게 사용된다.
모듈의 독립적 테스트를 보장하고 어떤 모듈에서 문제가 발생했는지 파악하기 쉽기 때문이다.
Mockito를 활용한 테스트코드 작성
Stubbing
일단 코드를 살펴보기 전에 stubbing에 대해서 알고 넘어가자
Stubbing은 Mock 객체를 생성 및 동작을 지정하는 것을 말한다.
기본적으로 Mock 객체는 기능이 없다.
이 때, 테스트할 모듈과 의존 관계에 있는 객체가 필요한 동작을 못한다면 문제가 발생한다.
이러한 상황에 stubbing을 통해 동작을 지정하면 테스트가 원활하게 이루어지도록 한다.
주로 아래와 같은 방식으로 stubbing을 지정한다.
when(함수(매개변수)).thenReturn(반환값)
Mocking을 활용한 테스트 코드 살펴보기
아래는 사용자 회원가입 로직에 대한 테스트 코드이다.
서비스 코드인 UserAuthService는 다음 객체들과 의존관계에 있다.
- UserRepository : DB에 접근하는 객체
- PasswordEncoder : 비밀번호를 암호화 및 비교하기 위해 사용하는 객체
- JwtUtils : 사용자 인증 및 인가를 위한 JWT 토큰을 생성 및 검증하기 위한 객체
@ExtendWith(MockitoExtension.class)
class UserAuthServiceTest {
@InjectMocks
private UserAuthService userAuthService;
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtUtils jwtUtils;
@Test
@DisplayName("회원가입 성공 테스트")
void testSignUpUserSuccess() {
// given
String email = "email";
UserCreateDto userCreateDto = UserCreateDto
.builder()
.email(email)
.nickname("nickname")
.password("password")
.role(UserRole.ROLE_GUEST)
.build();
// stub 1
when(userRepository.findByEmail(any(String.class)))
.thenReturn(Optional.empty());
// stub 2
when(userRepository.save(any(User.class)))
.thenReturn(userCreateDto.toEntity());
// when
UserAuthResponseDto res = userAuthService.signUpUser(userCreateDto);
// then
assert (res.getEmail().equals(email));
}
}
우선 위 테스트코드에서 사용된 어노테이션들을 각각 살펴보자.
- @ExtendsWith(MockitoExtension.class) : JUnit5에서 Mockito를 확장하여 사용할 수 있도록 한다.
- @ExtendsWith는 특정 클래스에 공통으로 적용할 기능이 있을 때 사용하는 어노테이션이다.
- 즉, 위 어노테이션을 사용하면 테스트 클래스에서 Mockito가 자동으로 Mock 객체를 생성 및 관리해준다.
- 해당 어노테이션 대신 각 테스트 메서드 실행 전에 MockitoAnnotations.initMocks(this)를 사용해도 된다.
- @Mock : 대상 클래스에 대한 Mock 객체를 생성하기 위해 사용한다.
- 해당 어노테이션 대신 Mockito.mock(대상 클래스) 메서드를 사용해도 된다.
- @InjectMocks : InjectMocks가 적용된 객체에 Mock 객체를 자동으로 주입해준다.
- 즉, 위 코드에서는 userAuthService에 JwtUtils, userRepository 등의 Mock 객체를 주입한다.
- @Test : JUnit5에서 특정 메서드가 테스트 메서드임을 나타내는 어노테이션이다.
- @DisplayName : JUnit5에서 테스트 이름을 명확하게 표시하는 어노테이션이다.
다음으로 위 코드에서 given-when-then에 대해서 알아보자
- given : userAuthService의 회원가입 메서드에 넘겨줄 인자를 설정한다 (userCreateDto)
- when : 실제 테스트할 대상인 signUpUser 메서드를 실행한다.
- then : 실행 결과, 기존 사용자와 회원가입된 사용자의 정보가 일치하는지 검증한다.
마지막으로 위 코드에서 stubbing이 어떻게 적용되었는지 알아보자.
- stub1 : signUpUser에는 회원가입할 사용자가 DB에 존재하는지 확인할 때, 기존 사용자 정보를 반환
- stub2 : DB에 사용자 저장 시 기존 사용자 정보 반환
즉 Mock을 활용하여 테스트할 때는, 테스트 대상 모듈 외 다른 모듈들은 정상 동작함을 가정한다.
다른 모듈의 오작동 테스트는 해당 모듈의 단위테스트에서 확인하면 될 일이다.
@SpringBootTest를 활용한 통합 테스트
@SpringBootTest의 용도
@SpringBootTest 어노테이션은 주로 통합 테스트에 사용되는 어노테이션이다.
실제 애플리케이션처럼 모든 빈과 컴포넌트를 초기화하여 실제 실행 환경과 가까운 테스트를 제공한다.
@SpringBootTest는 여러 속성을 제공하고 이를 통해서 테스트 환경을 설정할 수 있다.
- properties : 테스트 시 특정 프로퍼티 값을 설정할 수 있다.
- @SpringBootTest(properties = {"property,key=value"}
- classes : 테스트 시 로드할 특정 클래스나 빈을 지정할 수 있다. (기본값 : 모든 빈 로드)
- @SpringBootTest(classes = {TargetClass.class, ...})
- webEnvironment ; 테스트 시 웹 환경 설정을 지정할 수 있다.
- RANDOM_PORT : 랜덤 포트에서 실제 웹 환경을 제공
- MOCK : 모의 서블릿 환경 제공. 내장 서버는 실행하지 않는다
- DEFINED_PORT : 정의한 포트에서 실제 웹 환경을 제공
- NONE : 웹 환경을 제공하지 않음
- @SpringBootTest(webEnvironment=SpringBootTest.webEnvironment.MOCK)
@SpringBootTest vs @ExtendWith(MockitoExtension.class)
@SpringBootTest
- Spring Context를 로드함
- 비교적 실행 속도가 느리지만, 실제와 유사한 테스트 환경 제공
- 모든 계층을 포함한 통합적인 테스트가 가능함
- 주로 통합 테스트에 사용됨
@ExtendWith(MockitoExtension.class)
- Spring Context를 로드하지 않음
- 비교적 속도가 빠르고 가볍다.
- 종속성들을 Mock 객체로 대체하기 때문에 독립적 테스트가 가능
- 주로 단위 테스트에 사용됨
@SpringBootTest를 활용한 테스트 코드 살펴보기
아래는 기존 UserAuthService의 signUpUser를 @SpringBootTest를 활용하여 테스트한 것이다.
@SpringBootTest
class UserAuthServiceTest {
@Autowired
private UserAuthService userAuthService;
@Autowired
private UserRepository userRepository;
@BeforeEach
public void init(){
userRepository.deleteAll();
}
@Test
void TestSignUpUser() {
// given
String email = "email";
UserCreateDto userCreateDto1 = UserCreateDto
.builder()
.email(email)
.nickname("nickname")
.password("password")
.role(UserRole.ROLE_GUEST)
.build();
UserCreateDto userCreateDto2 = UserCreateDto
.builder()
.email(email)
.nickname("nickname")
.password("password")
.role(UserRole.ROLE_GUEST)
.build();
// when
UserAuthResponseDto res = userAuthService.signUpUser(userCreateDto1);
// then
assert (res.getEmail().equals(email));
}
}
Mocking을 활용한 테스트와 달리 UserAuthService에 필요한 의존성들은 빈 생성 과정에서 주입해준다.
일단 새롭게 사용하는 어노테이션에 대해서 알아보자.
- @Autowired : 타입을 기반으로 해당 타입과 일치하는 객체를 찾아 자동으로 필드에 주입해준다.
- 위 코드에서는 UserAuthService와 UserRepository 빈을 받아오기 위해 사용한다
- @BeforeEach : 각 테스트 메서드 전에 실행되는 메서드에 지정하는 어노테이션이다.
- 위 코드에서는 UserRepository를 활용하여 DB에 있는 데이터를 초기화하기 위해 사용한다.
결론적으로 기존 Mocking을 활용한 테스트처럼 서비스 코드 테스트를 위해서 사용한다
결정적 차이점은, @SpringBootTest 어노테이션을 사용하면 Mocking과 달리 Spring Context를 로드한다는 것이다.
마무리
이번에는 테스트 코드 작성에 대한 전반적인 개념에 대해서 알아보았다.
또한, Spring Boot에서는 단위 테스트와 통합 테스트 코드를 어떻게 작성하는지 알아보았다.
이번에 알아본 내용을 기반으로 조금씩 지식을 늘려보는 것이 좋을 것이다.
'Spring > 기초 개념' 카테고리의 다른 글
SpringBoot에서 Mockito로 소셜 로그인 단위 테스트 (0) | 2025.02.14 |
---|---|
Access Token와 Refresh Token을 활용한 인증과 인가 (0) | 2025.02.07 |
Spring Security로 JWT 토큰 인증 방식 구축하기 (1) | 2025.01.27 |
실습을 통해서 알아보는 Spring Security 기초 (0) | 2025.01.25 |
예시를 통해 알아보는 Spring Boot의 의존성 주입(DI) (1) | 2025.01.19 |