본인만의 테스트 하는 방법을 기술하려고 한다.
테스트 모듈들은 딱히 원본 아키텍쳐와 똑같을 필요는 없는 것 같다.
기본적인 기능들을 테스트하고, 그리고 개발 도중 애매한 것들을 바로바로 테스트할 수 있으면 된다.
그래서 아래와 같은 패키지로 구성했다.
basetest
실제 테스트 아니고 다른 테스트를 도와주는 Util들을 넣었다.
(커스텀 어노테이션, 추상 클래스, TestConfig 등)
controllers
컨트롤러 테스트로, RestDocument를 생성한다.
WebMvcTest 슬라이스에 해당한다.
repositories
JPA 관련 테스트를 진행한다. 부모/자식 의존 관계가 맞게 설정됐는지, 쿼리가 의도한대로 날라가는지 검증한다.
DataJpaTest 슬라이스에 해당한다.
기타
JwtFilter 나 다른 로직들을 테스트한다.
Test Slice에 따라 어떤 것들이 Import 되는지는 Spring 공식 문서에 나와있다.
https://docs.spring.io/spring-boot/appendix/test-auto-configuration/slices.html
컨트롤러 단을 테스트할 때 겹치는 로직이 많아서 따로 추상 클래스로 분리했다.
@Import(
TestConfig::class
SecurityConfig::class
)
@ExtendWith(restDocumentationExtension::class)
abstract class DefaultControllerTest {
@MockBean
lateinit var userDataDetailsService: UserDataDetailsService
@BeforeEach
fun mockMvc(webApplicationContext: WebApplicationContext, restDocumentation: RestDocumentationContextProvider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.alwaysDo<DefaultMockMvcBuilder> { it.response.setDefaultCharacterEncoding(StandardCharsets.UTF_8.toString()) }
.apply<DefaultMockMvcBuilder>(springSecurity())
.apply<DefaultMockMvcBuilder>(
documentationConfiguration(restDocumentation).snippets().withEncoding(StandardCharsets.UTF_8.toString())
)
.build()
}
}
해당 클래스를 상속 받으면, SecurityConfig이 그대로 적용되어 Web API를 바로 테스트해볼 수 있다.
각 테스트 클래스에 @WebMvcTest(controllers=[Controller::class])를 추가하면 된다.
해당 클래스의 mockMvc는 Rest Document를 생성할 수 있다. mockMvc.perform(..).andDo(document("..")) 로 Rest Document 를 생성할 수 있다.
이것은 각 필터를 테스트 한 다음에 하는 테스트이다.
Import에 TestConfig과 SecurityConfig 뿐 아니라 SecurityConfig이 참조하고 있는 클래스도 포함되어야 한다.
여기서 TestConfig 은 테스트에 필요한 Bean을 등록하는 클래스이다.
@WithUserDetail 이 참조하는 테스트용 UserDetailsService 클래스를 따로 만들고, 이를 빈으로 등록해야 한다.
class TestUser: UserDetailsService {
companion object {
const val USER = "user"
const val ADMIN = "admin"
}
override fun loadUserByUsername(username: String?): UserDetails {
return when(username) {
USER -> User(username, Role.USER)
ADMIN -> ...
}
}
}
@TestConfiguration
class TestConfig {
@Bean
@Primary
fun userDetailsService(): UserDetailsService {
return TestUser()
}
}
메인의 repository config 을 임포트 하는 어노테이션이다.
모든 repository test에서 하기 귀찮아서 새 어노테이션을 만들었다.
@WebMvcTest(controllers= [Controller::class])
class ControllerTest: DefaultControllerTest() {
@MockBean
lateinit var service: Service
@Test
fun `테스트 1`() {
// given
given(service.getFoo(any())).willReturn(true)
// when
val mvcResult = mockMvc
.perform(get("/foo"))
.andDo(document("/foo"))
.andReturn()
// then
assertThat(mvcResult.response.status).isEqualTo(HttpStatus.OK.value())
}
@Test
@WithUserDetails(TestUser.USER)
fun `테스트 2`() {
// given
given(service.getFoo(any())).willReturn(true)
// when
val mvcResult = mockMvc
.perform(get("/foo"))
.andDo(document("/foo"))
.andReturn()
// then
assertThat(mvcResult.response.status).isEqualTo(HttpStatus.FORBIDDEN.value())
}
}
controller 테스트에서 인가 과정을 생략하기 위해 @WithUserDetails 어노테이션을 사용할 수 있다.
json 입력, 출력을 위해 Gson 라이브러리를 사용한다.
@CustomDataJpaTest
class RepositoryTest(@Autowired val repository: Repository) {
@BeforeEach
fun setUp() {
val session: Session = entityManager.unwrap(Session::class.java)
statistics = session.sessionFactory.statistics
statistics.clear()
}
@Test
fun `테스트 1`() {
...
val count = statistics.prepareStatementCount
assertThat(count).isEqualTo(1L)
}
}