HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
Spring
Spring
/
👞
Spring Test Framework
👞

Spring Test Framework

참고
[ Doc ] Spring Boot Testing
Spring Boot 레이어별 테스트
 
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

Spring 가이드 [ testing-web]
  • 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로 매핑하기
  • BaeldungBaeldungGet JSON Content as Object Using MockMVC | Baeldung
    Get 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.

    BaeldungBaeldung
    • 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 (단위테스트)

[참고] Controller 단위테스트(@WebMvcTest, MockMvc)
[참고] MockMvc를 이용한 REST API의 Json Response 검증
@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가(얘만) 생각대로 작동하는지를 테스트하기 위함임
    • Spring MVC 의 infrastructure를 auto-configure하며 아래 리스트의 빈들을 스캔 ( Filter, Interceptor, Converter, @Controller 등의 빈들이 스캔됨 ) [참고 공식 문서 ]
      • @Controller
      • @ControllerAdvice
      • @JsonComponent
      • Converter
      • GenericConverter
      • Filter
      • HandlerInterceptor
      • WebMvcConfigurer
      • HandlerMethodArgumentResolver
       
    • @Component, @ConfigurationProperties 와 같은 Bean들은 컴포넌트 스캔 되지 않음
    • Controller에 대한 단위 테스트이기에 Controller의 의존성인 Service는 @MockBean 을 이용하여 mock 객체로 주입
  • @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"); } }
어플리케이션 실행 후, 커넥션을 기다리고 있는 상태인 것임. 그러면 TestRestTemplate에서 request를 날리는 형태로 테스트
@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 a Filter
  • 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’s SecurityMockMvcConfigurers.springSecurity()
  • @SpringBootTest 에서 특정 Bean 제외하는 방법
    • [ Baeldung ] Exclude Auto-Configuration classes in Spring Boot Tests
      1. @EnableAutoConfiguration 의 exclude 프로퍼티 사용 (AutoConfiguration 제외)
      1. 해당 Bean을 TestConfig로 null로 등록하면, 해당 Bean이 대체됨
        1. @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) )
    • root Array의 각 아이템별 모든 프로퍼티를 뽑아내는건 아무리 해도 잘 안됐음. objectMapper 통해서 deserializing 해서 비교
      • 봤었던 Github issue. How to traverse array which is the root element
  • 값이 다르다는 것을 비교할 때
    • resultActions.andExpectAll( jsonPath("$.data.accessToken", Matchers.not(token)) );
    • 이 때 Matchers는 org.hamcrest 것.

꿀팁

  • 에러 메시지 확인을 위해서는 아래와 같이 이용
    • 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 메서드들에 대해서는)
Video preview

@Transactional 사용 주의

1. 자동 롤백 됨

How does @Transactional work on test methods?
  • TransactionalTestExecutionListener
  • Automatic Rollback of Transactions in Spring Tests
💡
테스트가 @Transactional이면 각 테스트 메서드 수행별로 roll back을 디폴트로 하게 됨. 그러나 @SpringBootTest의 webEnvironment attribute가 RANDOM_PORT와 DEFINED_PORT인 경우는 실제 servlet 환경이어서 Http client와 서버가 서로 다른 스레드에서 동작하게 됨. 그래서 다른 Transaction이고 서버에서 시작된 transaction은 roll back 이 되지 않음

2. @Transactional이 없을 때 테스트가 통과하지 않을 수 있음

JPA 사용시 테스트 코드에서 @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; }
        Inner Configuration class이용
@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 이용하기

[ Medium ] integration testing of SpringBoot with MS SQL Server using TestContainers
Test containers for Java
[ Test Container ] Exposing host port is random
  • 위 참고 링크에 따르면 test container의 host port는 랜덤으로 배정된다고 함. 특정시킬 수가 없음.