참고
Mocking and Spying BeansSpring boot Auto-configured TestsRepository Layer 단위 테스트Service Layer 단위 테스트Controller Layer 테스트MockMvcResponse를 Object로 파싱하기@WebMvcTest (단위테스트)Mocking UserSpring Security Filter 제외 방법multipart/form-data request 하기Spring Test Framework의 cache를 이용한 테스트 코드 최적화@Transactional 사용 주의1. 자동 롤백 됨2. @Transactional이 없을 때 테스트가 통과하지 않을 수 있음Test Configuration 찾기@TestConfigurationTestContainer 이용하기
Mocking and Spying Beans
final
method를 mock하기 위해서는org.mockito:mockito-inline
을 application의 test dependency에 추가하기
Spring boot Auto-configured Tests
- object의 JSON serialization 과 deserialization 테스트를 하기 위해서는 @JsonTest를 이용가능함
@DataJpaTest
는 JPA 어플리케이션을 테스트하기 위해 사용함.@Entity
클래스를 스캔하고 Spring Data JPA repository 들을 configure함
@..Test 어노테이션을 하나의 테스트에 여러개 붙이는건 지원하지 않는 대신, 여러 slice test를 하기 위해서는 하나의 @...Test 어노테이션을 선택하고 @AutoConfigure.. 어노테이션을 이용하기
만약 @AutoConfigure.. 어노테이션을 @SpringBootTest 어노테이션과 함께 활용한다면 application의 slice 테스트를 위한 것은 아니고 auto-configured test bean의 일부만을 원할 때 이렇게 사용가능함
Repository Layer 단위 테스트
@DataJpaTest
- 디폴트로
@Entity
클래스와Spring Data JPA Repository
를 스캔함
- classpath에 embedded database가 이용가능하면 그것 configure 해줌
- 디폴트로 transactional이 적용되고 각각의 테스트 별로 rollback이 됨
- 표준 JPA
EntityManager
를 대신하여 테스트에 맞게 특별히 디자인 된TestEntityManager
빈을 주입시켜줌
Service Layer 단위 테스트
예제 코드
class UserServiceTest { private final UserRepository userRepository = mock(UserRepository.class); private final UserService userService = new UserService(userRepository); private final User user = new User("test-user@gmail.com", "abce12!@", Role.ROLE_USER); @Test void 이메일_중복으로_인한_저장_실패() { //given given(userRepository.save(any(User.class))).willReturn(user); userService.registerUser(user.getEmail(), user.getPassword()); willThrow(DataIntegrityViolationException.class).given(userRepository).save(any(User.class)); //when //then assertThatThrownBy( () -> userService.registerUser(user.getEmail(), user.getPassword())) .isInstanceOf(IllegalArgumentException.class) .hasMessage(Message.DUPLICATE_EMAIL.getContent()); then(userRepository).should(times(2)).save(any(User.class)); } }
Controller Layer 테스트
MockMvc
Response를 Object로 파싱하기
- TypeReference를 이용해 mockMvc response를 object로 매핑하기
BaeldungGet JSON Content as Object Using MockMVC | Baeldung
Get JSON Content as Object Using MockMVC | Baeldung
Explore several ways to get JSON content as an object using MockMVC and Spring Boot.
val response = mockMvc.perform( get(endpoint, teamId) .header(Header.USER_ROLE, UserRole.LEADER.name) .header(Header.CORP_TYPE, CorpType.UPLUS.name) .header(Header.USER_ID, leaderUser.userId)) .andReturn().response.contentAsString val baseGenericResponse = jacksonObjectMapper().readValue( response, object: TypeReference<BaseGenericResponse<List<UserStateInfoResponse>>>(){} )
@WebMvcTest (단위테스트)
@WebMvcTest public class WebLayerTest { @Autowired private MockMvc mockMvc; @Test public void shouldReturnDefaultMessage() throws Exception { this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) .andExpect(content().string(containsString("Hello, World"))); } }
- 해당 어노테이션을 사용하면 Spring Boot는 전체 컨텍스트를 실행하는 것이 아닌 web layer만 실행함. 다수의 controller중 하나만 생성(instantiated)도 가능함
- Spring MVC controller가(얘만) 생각대로 작동하는지를 테스트하기 위함임
@Controller
@ControllerAdvice
@JsonComponent
Converter
GenericConverter
Filter
HandlerInterceptor
WebMvcConfigurer
HandlerMethodArgumentResolver
- @Component, @ConfigurationProperties 와 같은 Bean들은 컴포넌트 스캔 되지 않음
- Controller에 대한 단위 테스트이기에 Controller의 의존성인 Service는
@MockBean
을 이용하여 mock 객체로 주입
Spring MVC 의 infrastructure를 auto-configure하며 아래 리스트의 빈들을 스캔 ( Filter, Interceptor, Converter, @Controller
등의 빈들이 스캔됨 ) [참고 공식 문서 ]
- @WebMvcTest는 기본적으로 MockMvc와 Spring Security를 auto configure 함(DefaultSecurityFilterChain이 등록됨)
여기에 CsrfFilter도 포함되어, post 요청 보낼 시에는 with(csrf)를 붙여서 써주어야 함 (안붙이면 403 Forbidden
에러 발생)
ResultActions result = mvc.perform(MockMvcRequestBuilders.post("/departments") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON).with(SecurityMockMvcRequestPostProcessors.csrf()));
Mocking User
testImplementation('org.springframework.security:spring-security-test') testImplementation("org.springframework.boot:spring-boot-starter-test")
[ Spring doc ] Runnning a test as a user in Spring MVC test
[ Spring doc ]Testing Method Security (@WithMockUser 등 어노테이션 사용방법 정리)
- @WithMockUser, @WithSecurityContext 등
Spring Security Filter 제외 방법
@WebMvcTest 에서 excludeAutoConfiguration 설정에 SecurityAutoConfiguration을 설정하기
@WebMvcTest(controllers = {DepartmentController.class}, excludeAutoConfiguration = {SecurityAutoConfiguration.class})
- 또는
MockMvcBuilders
를 이용해서 custom 하게 만들면서 SecurityFilter는 제외
@BeforeEach void setup(WebApplicationContext webApplicationContext, RestDocumentationContextProvider provider) throws NoSuchMethodException { ApiUrlToPacketIdConverter converter = new ApiUrlToPacketIdConverter(); this.mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .addFilter(new AddUnityPacketIdFilter(converter)) .build(); }
webEnvironment
:RANDOM_PORT, TestRestTemplate
: 실제로 애플리케이션 전체를 구동하는 테스트 방법
package com.example.testingweb; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.beans.factory.annotation.Value; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class HttpRequestTest { @Value(value="${local.server.port}") private int port; @Autowired private TestRestTemplate restTemplate; @Test public void greetingShouldReturnDefaultMessage() throws Exception { assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/", String.class)).contains("Hello, World"); } }
@AutoconfigureMockMvc @SpringBootTest
: 애플리케이션을 실행하지 않고, Spring이 HTTP request를 handle 하고 controller에 넘겨주는 것 까지만 테스트 하는 방법
MockMvc
가 주입되기 위해서는 @SpringBootTest
에서는 @AutoConfigureMockMvc
가 사용되어야 함 && WebEnvironMent None이 되면 해당 MockMvc 못찾음
package com.example.testingweb; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc public class TestingWebApplicationTest { @Autowired private MockMvc mockMvc; @Test public void shouldReturnDefaultMessage() throws Exception { this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) .andExpect(content().string(containsString("Hello, World"))); } }
- WebEnvironment (참고 : 스프링 부트 테스트)
- RANDOM_PORT
- MOCK : 내장 톰캣 구동 안함
- DEFINED_PORT
- NONE
MockMvcBuilders
를 이용하여 MockMvc 커스텀 정의하는 방법
@ExtendWith(RestDocumentationExtension.class) @SpringBootTest @Import(RestDocsConfig.class) @ActiveProfiles("test") public abstract class AbstractControllerTest { @Autowired protected RestDocumentationResultHandler restDocs; protected MockMvc mvc; @BeforeEach public void setup(RestDocumentationContextProvider provider, WebApplicationContext context) { this.mvc = MockMvcBuilders.webAppContextSetup(context) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .apply(SecurityMockMvcConfigurers.springSecurity()) .alwaysDo(restDocs) .addFilter(new CharacterEncodingFilter("UTF-8", true)) .build(); }
[ Spring Security Docs ] Setting up MockMvc and Spring Security
SecurityMockMvcConfigurers.springSecurity()
요거 apply 안해주면 mvc 가 Sprint Security Filter를 포함하지 않아서 security에 대한 테스트 안됨
- To use Spring Security with Spring MVC Test, add the Spring Security
FilterChainProxy
as aFilter
- You also need to add Spring Security’s
TestSecurityContextHolderPostProcessor
to support Running as a User in Spring MVC Test with Annotations. To do so, use Spring Security’sSecurityMockMvcConfigurers.springSecurity()
@SpringBootTest
에서 특정 Bean 제외하는 방법@EnableAutoConfiguration
의exclude
프로퍼티 사용 (AutoConfiguration 제외)- 해당 Bean을 TestConfig로 null로 등록하면, 해당 Bean이 대체됨
@SpringBootTest(classes = { TestConfig.class })
JsonPath
사용법 참고 : Jayway JsonPath Github
- mockMvc를 이용하여 반환된 json이 null인지 확인하는 방법
andExpect(jsonPath("$").doesNotExist())
- array 의 특정 필드를 뽑아내는 방법
- jsonPath(
"$.data[*].role"
) : data 안에 리스트의 모든 원소의 role 필드를 뽑아내서 JSONArray로 만들어줌 - 그후 해당 array를 비교하는 방법
MockMvcResultMatchers.jsonPath("$.data[*].role", Matchers.containsInAnyOrder(adminRole, adminRole, adminRole) )
- 봤었던 Github issue. How to traverse array which is the root element
- 값이 다르다는 것을 비교할 때
resultActions.andExpectAll( jsonPath("$.data.accessToken", Matchers.not(token)) );
꿀팁
- 에러 메시지 확인을 위해서는 아래와 같이 이용
andExpect(MockMvcResultMatchers.
status
().reason("asdfasdf"))
multipart/form-data request 하기
[ 블로그] 참고
byte[] bytes = readFileAndGetBytes("/right_banner.png"); MockMultipartFile file = new MockMultipartFile("file", "raw_file.png", "image/png", bytes); // 첫번째 argument가 controller에서 받는 변수이름이어야 함 String saveFileName = "banner.png"; //when mvc.perform(multipart(HttpMethod.POST, ApiUrlComponent.MEDIA_ENDPOINT) .file(file) .queryParam("mediaTypeId", String.valueOf(MediaType.TOWN_HALL_RIGHT_BANNER.getId())) .queryParam("fileName", saveFileName) .header(AUTHORIZATION_HEADER, adminToken));
Spring Test Framework의 cache를 이용한 테스트 코드 최적화
[ Spring Framework ] Context Caching
org.springframework.test.context.cache
모듈의 log level을DEBUG
로 설정하면 context cache의 statistics를 확인할 수 있음
[DefaultContextCache@44173c2b size = 14, maxSize = 32, parentContextCount = 0, hitCount = 2105, missCount = 14] # missCount가 14회나 된다는 말
- 테스트 간에 설정이 공통이라면 Spring Test Framework는 ApplicationContext를 다시 만들지 않고 캐시된 context를 활용함
- 그러나, 같은 설정을 상속하더라도 하위 클래스에서 MockBean 같은 필드가 추가되면 ApplicationContext가 다시 만들어진다고 함
- 그리고 Acceptance Test에 대해서 DB 환경을 공유함 (Get 메서드들에 대해서는)

@Transactional 사용 주의
1. 자동 롤백 됨
테스트가 @Transactional이면 각 테스트 메서드 수행별로 roll back을 디폴트로 하게 됨. 그러나 @SpringBootTest의 webEnvironment attribute가 RANDOM_PORT와 DEFINED_PORT인 경우는 실제 servlet 환경이어서 Http client와 서버가 서로 다른 스레드에서 동작하게 됨. 그래서 다른 Transaction이고 서버에서 시작된 transaction은 roll back 이 되지 않음
2. @Transactional이 없을 때 테스트가 통과하지 않을 수 있음
- 서비스 코드에서 @Transactional을 안 붙이고, 테스트 코드에서 @Transactional을 붙이면 테스트는 통과하지만 런타임시 오류가 발생할 수 있다!
- 또한, 영속성 컨텍스트가 유지되기 때문에 영속성 컨텍스트를 없앴을 때에 테스트가 통과하지 않을 수도 있음
Test Configuration 찾기
- 찾는 방법
@ContextConfiguration
을 사용해서 어떤 @Configuration을 로드할지 설정- nested @Configuration을 테스트 클래스 안에 사용
public class RestDocsTestSupport extends BaseControllerTest { @Configuration static class RestDocsConfig { @Bean public RestDocumentationResultHandler write() { return MockMvcRestDocumentation.document( "{class-name}/{method-name}", Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) ); } } @Autowired RestDocumentationResultHandler restdocs; }
@SpringBootTest
의 경우 classes 항목에 클래스를 넣어주면 알아서 해당 Component를 ApplicationContext를 만들 때 load하게 됨
- Document : The component classes to use for loading an ApplicationContext. Can also be specified using
@ContextConfiguration(classes=...)
. If no explicit classes are defined the test will look for nested @Configuration classes, before falling back to a @SpringBootConfiguration search.
@*Test
annotation들이 알아서 primary configuration을 찾아줌
- 만약에 primary configuration이 아닌 커스터마이징을 원한다면 nested
@TestConfiguration
을 활용하기
@TestConfiguration
@Configuration that can be used to define additional beans or customizations for a test.
Unlike regular
@Configuration
classes the use of @TestConfiguration
does not prevent auto-detection of @SpringBootConfiguration.- 테스트 환경에서 필요한 빈들을 등록할 수 있도록 도와주는 어노테이션
- 해당 어노테이션으로 클래스 생성후 이용하려면 Import 해서 이용해야함
- https://reflectoring.io/spring-boot-testconfiguration/
- 만약, 동일한 Bean을 Overriding 하고자 한다면
spring.main.allow-bean-definition-overriding
의 값을 true로 설정해야 함. 그렇지 않으면 BeanDefinitionOverrideException 발생
참고 코드
@TestConfiguration public class TestConfig { @Bean public RestDocumentationResultHandler resultHandler() { return MockMvcRestDocumentation.document( "{class-name}/{method-name}", Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) ); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } } @ExtendWith(RestDocumentationExtension.class) @SpringBootTest(classes = { TestConfig.class }) @ActiveProfiles("test") public interface BaseControllerTest { }
TestContainer 이용하기
[ Test Container ] Exposing host port is random
- 위 참고 링크에 따르면 test container의 host port는 랜덤으로 배정된다고 함. 특정시킬 수가 없음.