개요
테스트 코드를 작성하다보면 중복코드가 많이 발생한다. 예를 들어 서비스 레이어를 테스트하는 슬라이스 테스트인 경우 @ExtendWith(MockitoExtension.class)를 통해 Mokcito를 활성화시켜주고, 필요하다면 @ActiveProfiles를 통해 프로필을 설정하는 등의 코드들이 계속 생겨나는 모습을 볼 수 있다.
본 포스팅에서는 통합 테스트/유닛/레포지토리 테스트 코드 작성 시 코드 로직 작성에만 집중할 수 있도록 각 테스트 대상에 맞는 통합 어노테이션을 만들고 적용하는 과정에 대해 기술한다.
@TestEnvironment
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ActiveProfiles("test")
@TestConstructor(autowireMode = AutowireMode.ALL)
public @interface TestEnvironment {}
테스트 환경임을 명시하는 어노테이션이다.
- @ActiveProfiles: 활성화시킬 프로필을 명시한다. 나는 test환경을 위한 application.yml파일을 따로 관리하고 있고 해당 파일을 통해 설정값을 읽어오기 위해 사용한다.
- @TestConstructor: autowireMode를 ALL로 설정함으로써 테스트 코드에서 @Autowired 어노테이션을 생략하기 위해 사용한다.
@UnitTest
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(MockitoExtension.class)
@TestEnvironment
public @interface UnitTest {}
레이어 별 테스트를 진행할 때 사용한다. 일반적으로 서비스 레이어에 해당할텐데, 모킹이 필요할 때 사용한다고 생각하면 된다. 물론 모킹이 필요하지 않을때는 굳이 사용할 필요가 없다.
- @ExtendWith: Mockito를 활성화시키기 위해 사용한다.
- @TestEnvironment: 테스트 환경임을 명시한다.
레포지토리 테스트
RepositoryTestConfig
@TestConfiguration
public class RepositoryTestConfig {
@PersistenceContext private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
먼저 @TestConfiguration 어노테이션을 붙인 레포지토리 테스트 관련 설정 클래스를 하나 생성한다. 아래에서 설명하겠지만, 레포지토리 테스트는 @DataJpaTest 어노테이션을 사용하는데 해당 어노테이션은 @SpringBootTest와는 다르게 스프링의 모든 빈을 로드하지 않는다. 따라서 위처럼 @TestConfiguration을 붙인 클래스를 하나 생성하고 실제 사용처에서 @Import를 통해 빈으로 등록해줘야 사용할 수 있다.
@RepositoryTest
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest
@TestEnvironment
@AutoConfigureTestDatabase(replace = Replace.NONE)
@ImportAutoConfiguration(DataSourceConfig.class)
@Import(RepositoryTestConfig.class)
public @interface RepositoryTest {}
레포지토리 레이어를 테스트할 때 사용한다. 일반적으로 Data JPA 또는 QueryDSL을 통해 생성한 레포지토리를 테스트할 때 사용한다.
- @DataJpaTest: @SpringBootTest와는 다르게 JPA에 관련된 로드하기 때문에 비교적 속도가 빠르다. 또한 내부적으로 @Transactional 어노테이션이 달려있기 때문에 각 테스트가 종료되면 자동으로 롤백이 수행된다. 관련해서 재밌는 영상이 있는데 토스의 김재민님이 공개한 유튜브 영상이다. 해당 영상에서는 @Transactional 을 테스트에 사용하지 말라고 권장한다. @RepositoryTest 에서도 @DataJpaTest 어노테이션을 제거하고 영상에서 소개한 대로 적용이 가능하다. 다만 본 포스팅에서 이야기하는것은, 각 레이어 또는 목적에 맞게 테스트 관련 어노테이션을 통합하여 사용하는 것이라는 것을 기억해줬으면 한다.
- @AutoConfigureTestDatabase: NONE으로 설정하면 @ActiveProfiles에 설정한 프로필 설정값에 따라 DataSource를 적용시킨다. 나는 내장 DB가 아닌 실제 MySQL을 도커로 띄워놓고 테스트를 진행하기 때문에 NONE으로 설정했다. 기본값은 Any이며 해당 값 사용 시, 자동으로 내장된 임베디드 DB를 사용한다.
- @ImportAutoConfiguration: 현재 DB관련 설정을 Reader/Writer를 분리시킨 Routing DataSource 형태로 사용하고 있다. 그리고 해당 설정이 DataSourceConfig 클래스에 들어있는데, 해당 클래스를 빈으로 등록해주기 위해 사용한다. 위에서 말했듯이 @DataJpaTest는 JPA관련 빈만 로드하기 때문에 이 부분이 필수적으로 들어가야한다.
- @Import(RepositoryTestConfig.class): 위에서 생성한 레포지토리 테스트 관련 설정 파일을 임포트한다. 이렇게해야 테스트 코드에서 동일한 EntityManager를 사용하기 때문에 테스트 시 문제가 발생하지 않는다.
통합 테스트
통합 테스트는 하나의 어노테이션이 아니라 추가적인 클래스도 생성해서 작업한다.
@IntegrationTest
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@AutoConfigureMockMvc
@TestEnvironment
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(RestDocumentationExtension.class)
@TestExecutionListeners(
value = {IntegrationTestExecutionListener.class},
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface IntegrationTest {}
- @SpringBootTest: 통합 테스트를 위해 모든 스프링 빈을 띄우기 위해 사용한다.
- @AutoConfigureMockMvc: MockMvc를 사용하기 위해 사용한다. 참고로 @WebMvcTest와 같이 사용할 수 없다.
- @ExtendWith: 내부적으로 Spring Rest Docs를 적용하기 위해 사용한다.
- @TestExecutionListeners: 각 테스트 코드는 독립적으로 동작해야하기 때문에 테스트가 종료된 후 데이터베이스를 Clean up해주는 작업이 필요하다. 그리고 스프링에서는 AbstractTestExecutionListener를 상속받은 후 원하는 메소드를 오버라이드하여 사용할 수 있도록 방법을 제공해주고 있다. 관련해서는 링크를 참고한다.
BaseIntegrationTest
@IntegrationTest
public class BaseIntegrationTest {
@Autowired protected MockMvc mockMvc;
@Autowired protected ObjectMapper objectMapper;
@Autowired EntityManager em;
protected JPAQueryFactory queryFactory;
@BeforeEach
void setUp(WebApplicationContext context, RestDocumentationContextProvider provider) {
queryFactory = new JPAQueryFactory(em);
this.mockMvc =
MockMvcBuilders.webAppContextSetup(context)
.addFilter(new CharacterEncodingFilter("UTF-8", true))
.alwaysDo(print())
.alwaysDo(
MockMvcRestDocumentation.document(
"{class-name}/{method-name}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())))
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.build();
}
}
통합 테스트는 BaseIntegrationTest 클래스를 상속받아서 구현한다. 내부적으로 사용할 MockMvc, ObjectMapper, EntityManager와 JPAQueryFactory를 주입시켜두었다. 또한 setUp() 메소드에서 Spring Rest Docs를 적용하기 위한 코드를 넣어줬다. 해당 클래스를 상속받은 테스트 클래스는 위에서 정의한 것 중 필요한 객체를 가져다가 쓰기만 하면 된다.
정리
- 통합 테스트: BaseIntegrationTest 클래스를 상속받아서 사용
- 내부에 정의해둔 MockMvc, ObjectMapper등을 상속받은 클래스에서 가져다가 쓸 수 있음
- 슬라이스 테스트: @UnitTest 어노테이션 사용
- Mockito를 활성화 시켜두었기 때문에 @Mock, @InjectMocks를 통해 모킹 테스트 가능
- 레포지토리 테스트: @RepositoryTest 어노테이션 사용
- 본 포스팅의 코드 상 내장 DB를 사용하지않고 application.yml에 설정되어있는 구성에 따라 DataSource를 구성하기 때문에 내장 DB를 사용하려면 추가적인 수정이 필요
- @Transactional 어노테이션을 통해 각 테스트 이후 자동 롤백
마치며
본문에서는 컨트롤러 레이어에 대한 슬라이스 테스트를 수행하지 않았다. 통합 테스트를 통해 한번에 수행되기 때문에 제외시켰는데, 만약 해당 레이어도 테스트를 진행하고 싶다면 @ControllerTest와 같은 어노테이션을 생성하고 필요한 부분을 조합하여 사용하면 되지 않을까 싶다.
'Coding > Java Spring' 카테고리의 다른 글
스프링 통합 테스트를 위한 AbstractTestExecutionListener (0) | 2023.08.30 |
---|---|
Reactive Streams 개념 정리 (0) | 2023.08.23 |
스프링 프로젝트 시작을 위한 컨벤션 설정 (0) | 2023.08.22 |
DDD 이벤트 스토밍 - 크림 클론 프로젝트 (0) | 2023.08.18 |
Webflux Function Endpoints 정리 (0) | 2023.08.17 |