[테스트] 통합 테스트와 슬라이스 테스트

도현김·2024년 4월 12일
post-thumbnail

1. 통합 테스트


  • 실제 운영 환경에서 사용될 클래스들을 통합하여 테스트 한다.
  • 단위 테스트와 같이 기능 검증을 위한 것이 아니라 spring framework에서 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용 한다.

1.1 @SpringBootTest

  • 스프링 부트는 @SpringBootTest 어노테이션을 통해 스프링부트 어플리케이션 테스트에 필요한 거의 모든 의존성을 제공한다.
  • @SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 어노테이션 이다.
    • JUnit4 : @RunWith(SpringRunner.class)
    • JUnit5 : 기입 X
  • 장점
    • 애플리케이션의 설정, 모든 Bean을 모두 로드하기 때문에 운영 환경과 가장 유사한 테스트가 가능하다.
    • 전체적인 Flow를 쉽게 테스트 가능하다.
  • 단점
    • 애플리케이션의 설정, 모든 Bean을 모두 로드하기 때문에 시간이 오래걸리고 무겁다.
    • 테스트 단위가 크기 때문에 디버깅이 어려운 편이다.
    • 의존성 주입이 하나라도 잘못되면 springBootTest가 붙은 모든 테스트는 통과되지 않는다.
    • bulid 폴더와 java 코드 폴더의 동기화에 대한 오류가 발생하여 의존성 주입할 클래스를 찾을 수 없다는 오류가 발생할 수도 있다.
      • springBootTest 실행 중 ‘cannot be opened because it does not exist’ 오류
      • 이는 클래스명이나 구조가 바꼈는데 build 폴더 내의 파일이 동기화가 되어 있지 않기 때문에 발생하는 문제다.
      • 만약 테스트시 이러한 문제가 생기면 모든 모듈의 build 폴더를 다 삭제시켜주고 재빌드 하면 된다.

1.1.1 property

  • 프로퍼티를 {key=value} 형식으로 직접 추가할 수 있다.
  • 프로퍼티는 기본적으로 클래스 경로상의 application.properties(또는 application.yml)를 통해 애플리케이션 설정을 수행 한다.
  • ex) @SpringBootTest(properties = "testValue=test")
  • @Value("${testValue}") 를 통해 코드에서 property 값을 사용할 수 있다.
  • value 또한 테스트가 실행되기 전에 프로퍼티를 주입시킬 수 있다. 하지만 property와 함께 사용할 수는 없다.

1.1.2 webEnvironment

  • 웹 테스트 환경 구성이 가능하다.

  • webEnvironment 파라미터를 이용하여 손쉽게 웹 테스트 환경을 선택할 수 있다.

  • ex) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.*RANDOM_PORT*)

  • Mock (기본 값)

    • 실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려울 경우, 가짜 객체를 만들어 사용한다.
    • WebApplicationContext를 로드하며 내장된 서블릿 컨테이너가 아닌 Mock 서블릿을 제공한다.
    • 별도로 지정하지 않으면 기본 값은 Mock 서블릿을 로드하여 구동하게 된다.
    • @AutoConfigureMockMvc 어노테이션을 함께 사용하면 별다른 설정 없이 간편하게 MockMvc를 사용한 테스트를 진행할 수 있다.
  • RANDOM_PORT

    • EmbeddedWebApplicationContext를 로드하며 실제 서블릿 환경을 구성 한다.
    • 무작위 port listen
  • DEFINED_PORT

    • RAMDOM_PORT와 동일하게 실제 서블릿 환경을 구성하지만, 포트는 애플리케이션 프로퍼티에서 지정한 포트를 listen 한다.
  • NONE

    • 기본적인 ApplicationContext를 로드한다.

1.1.3 classes

  • @SpringBootTest는 기본적으로 SpringApplicationConfiguration 클래스를 찾아 로드한다.
    • @SpringBootApplication 가 위치한 클래스를 기준으로 로드한다.
    • 이 어노테이션은 대체적으로 어플리케이션을 실행하는 곳에 위치해 있다.
    • 그러나 어플리케이션을 실행하지 않는 모듈에서는 특정 위치를 지정해서 @SpringBootApplication를 위치시켜야 그 곳을 기준으로 정해 @SpringBootTest테스트가 실행된다.
  • @SpringBootTest에서 classes 속성을 사용하면 classes 에 적힌 @Configuration 클래스나 기타 클래스만 Application Context에 로드된다.
    • 위의 @SpringBootApplication 기준으로 모든 Bean을 로드하고 싶지 않다면 사용하면 된다.
    • 따라서 전체 애플리케이션의 설정, 모든 Bean을 모두 로드하지 않고 가볍게 테스트를 진행할 수 있다.
    • 하지만 설정이나 의존성 주입이 누락된다면 오류를 발생시킨다.

1.1.4 exclude

  • exclude 속성을 사용하면 @SpringBootApplication로 인해 자동으로 설정되는 모든 bean 중 특정 자동 설정을 제외할 수 있습니다.
    • 특정 bean을 로드하지 않을 수 있다.

1.2 TestRestTemplate

  • webEnvironment 설정 시 그에 맞춰서 자동으로 설정되어 빈이 생성되며, RestTemplate의 테스트를 처리가 가능하다.
    • webEnvironment 가 NONE이면 사용할 수 없다.
  • Spring 4.x 이후부터 지원하는 Spring의 HTTP 통신 템플릿이다.
  • HTTP 요청 후 Json, xml, String 과 같은 응답을 받을 수 있는 템플릿이다.
  • Http request를 지원하는 HttpClient를 사용한다.
  • ResponseEntity와 Server to Server 통신하는데 자주 쓰인다.
    • ResponseEntity는 응답 처리시 값 뿐만 아니라 상태 코드, 응답 메세지 등을 포함하여 리턴 가능하다. HttpEntity를 상속받기 때문에 HttpHeader와 body를 가질 수 있다.
  • Header, Content-Type등을 설정하여 외부 API 호출

1.3 @MockBean

  • Mock 객체를 빈으로써 등록할 수 있다.
  • @MockBean은 Spring의 ApplicationContext는 Mock 객체를 빈으로 등록하며, 혹시 @MockBean으로 선언된 객체와 같은 이름과 타입으로 이미 빈이 등록되어있다면 해당 빈은 선언한 @MockBean으로 대체된다.

1.4 @Transactional

  • 테스트 완료 후 자동으로 rollback 처리 한다.
    • spring-boot-test는 단순히 spring-test를 확장한 것이기 때문에
    • @Test 와 함께 @Transactional 을 함께 사용하면 테스트가 끝날 때 rollback 처리한다.
  • WebEnvironment.RANDOM_PORT, DEFINED_PORT를 사용하면 트랜잭션이 롤백되지 않는다.
    • 실제 테스트는 별도의 스레드에서 진행되기 때문이다.

1.5 @ActiveProfiles

  • 프로파일 전략을 사용 중이라면 원하는 프로파일 환경값 설정이 가능 하다.

1.6 @LocalServerPort

  • 통합 테스트에서 사용되는 스프링 부트 애플리케이션의 임의 포트 번호를 가져오기 위한 어노테이션이다.

1.7 @WithMockUser, @WithAnonymousUser, @WithUserDetails

  • TestRestTemplate를 통한 통합 테스트에서도 @WithMockUser, @WithAnonymousUser, @WithUserDetails를 통해서 시큐리티 필터를 통과할 수 있다.

아래의 코드는 위의 내용들을 조합해서 통합 테스트를 하는 예시 코드이다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GreetingControllerIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
		@WithMockUser
    public void testGreetEndpoint() {
        // Given
        String url = "http://localhost:" + port + "/api/greet/John";

        // When
        String response = restTemplate.getForObject(url, String.class);

        // Then
        assertThat(response).contains("Hello, John!");
        // Additional assertions or verifications if needed
    }
}

2. Controller 테스트



2.1 테스트 방법

2.1.1 @WebMvcTest

  • 모의 테스트

  • @AutoConfigureMockMvc가 내장되어 있기 때문에 MockMVC를 자동 구성함.

  • @MockBean을 통한 스터빙이 필요하다.

    • 가벼운 테스트가 가능
    • 웹에서 테스트하기 힘든 컨트롤러를 테스트 하는데 적합
      • ex) 결제 모듈 API를 콜하며 안되는 상황에서 Mock을 통해 가짜 객체를 만들어 테스트 가능.
    • 모의 테스트를 하기 때문에 Controller 레벨 이외의 클래스는 사용할 수 없다.
    • 요청부터 응답까지 모든 테스트를 Mock 기반으로 테스트하기 때문에 실제 환경에서는 제대로 동작하지 않을 수 있다.
  • SpringBootTest와 다르게 Controller 관련 테스트 설정만 로드한다.

    • 통합 테스트보다 빠르다.
    • 사용하기 위해 테스트할 특정 컨트롤러 클래스를 명시해야한다.
      • ex) @WebMvcTest(Controller.class)
    • @Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterHandlerInterceptorWebMvcConfigurerHandlerMethodArgumentResolver
      • @Component, @Service, @Repository…는 등록하지 않는다.
    • Controller와 관련된 테스트 설정이 아닌 경우, 직접 스프링 컨테이너에 클래스를 로드시켜줘야함.
      • 테스트 대상 클래스가 의존하는 것이 인터페이스라면 @TestConfiguration 사용해서 안에서 @Bean을 만들고 직접 주입해줘야 함.
      • 테스트 대상 클래스가 의존하는 것이 클래스라면 @Import 어노테이션을 통해 컨테이너에 로드해줘야 함.

2.1.2 @SpringBootTest + @AutoConfigureMockMvc

  • 실제 테스트 (통합 테스트)
  • @AutoConfigureMockMvc를 통해 MockMVC를 자동 구성함.
  • @WebMvcTest와 다르게 애플리케이션의 전체 클래스를 로드한다.
    • 직접 스프링 컨테이너에 클래스를 로드할 필요가 없다.
    • @Component, @Service, @Repository… 모두 로드함
    • 하지만 의존성 문제나 빈 주입 단계에서 오류가 발생하면 @SpringBootTest가 있는 모든 테스트 클래스는 오류가 발생한다.

2.2 Spring Security 테스트 설정

  • @WithMockUser, @WithAnonymousUser, @WithUserDetails가 있다.

  • @WebMvcTest@SpringBootTest + @AutoConfigureMockMvc와 함께 controoler 테스트에서 사용한다.

  • 인증된 사용자 or 미인증된 사용자를 만들어서 securityFilter를 통과하게 해주는 어노테이션이다.

  • 특정한 사용자 또는 권한으로 시뮬레이트된 사용자로서의 인증을 제공한다.

  • securityFilter를 무시하는 것과는 관련이 없다.

  • mockmvc를 사용할 때도 securityFilter를 거쳐야 하는데 번거로운 인증 작업을 간편하게 처리해준다.

  • 컨트롤러 테스트에서 Spring Security 설정하는 방법은 두가지가 있다.

    1. 기존에 만들어 놓았던 SecurityConfig 설정을 사용하면서 @WithMockUser와 같은 어노테이션을 사용하는 방법. 이는 시뮬레이트된 사용자로서의 인증을 하는 방법이다.
    2. 테스트용 SecurityConfig 클래스를 만들어서 HttpSecurity를 설정해서 모든 경로를 다 열어두는 방법. (컨트롤러 테스트는 컨트롤러에 대한 테스트에 집중하면 되기 때문

2.2.1 @WithMockUser

  • 별도의 UserDetailsService와 같은 스텁을 제공하지 않아도 간단하게 인증정보를 설정하기 위한 어노테이션.
  • @WithMockUser 어노테이션은 Controller 테스트 시에 Spring Security에 설정한 인증 정보를 제공해 주는 역할을 함.
  • 사용자 인증 정보를 담은 Authentication을 UsernamePasswordAuthenticationToken으로 넣어주고, Principal은 User 객체에 넣어 SecurityContext에 보관해 준다.
  • username, password, role의 기본 값은 각각 “user”, “password”, “ROLE_USER” 이다.
  • username, password, role의 기본값을 변경할 수 있다.
    • ex) @WithMockUser(username = "xxx", password = "xxx", roles = {"xxx"})

2.2.2 @WithAnonymousUser

  • 익명유저의 인증정보를 설정하기 위한 어노테이션
  • @WithMockUser와 반대로 인증되지 않은 사용자를 만들어서 사용해 준다.
  • @WithMockUser에서 사용된 UsernamePasswordAuthenticationToken이 아닌 AnonymousAuthenticationToken을 Authentication에 담아서 보내준다.

2.2.3 @WithUserDetails

  • UserDetailsService를 통해서 유저정보를 취득하여 설정하기 위한 어노테이션
  • 사용자 정보가 @WithMockUser나 @WithAnonymousUser와 동일한 경우는 바로 사용하면 되지만, 추가적인 정보가 있다면 UserDetailsService를 구현한 서비스에서 사용자 정보를 load 하도록 설정해 줄 수 있다.
  • 직접 구현한 UserDetailService를 찾아 사용 정보를 Principal로 설정해준다. 물론 User 객체도 UserDetails를 구현해야 한다.
  • 실제로 해당 username을 가진 user가 존재해야한다.
  • @BeforeEach, @BeforeAll과 함께 사용할 때 발생하는 문제
    • SecurityContext는 default로 TestExecutionListener.beforeTestMethod로 설정이 되어있다.

    • 즉 @Before 전에 @withUserDetails이 동작해서 SecurityContext 안에 넣으려고 하기 때문에 실제 user객체가 생기기 전에 해당 user를 찾아 SecurityContext가 제대로 생기지 못하고 테스트가 실패함.

    • 이 문제는 setupBefore를 TestExecutionListener.beforeTestExecution로 설정하는 것으로 해결 가능함.

      @WithUserDetails(value = "example2@naver.com", setupBefore = TestExecutionEvent.TEST_EXECUTION)

3. Service 테스트


  • service 테스트는 따로 어노테이션이 없고 대부분 앞선 게시글의 Mockito를 사용해서 모의 테스트를 진행한다.

4. Repository 테스트


4.1 @DataJpaTest

  • Repository 슬라이스 테스트
  • DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능
  • 내장형 데이터베이스를 사용 (in-memory embedded database)
  • @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성
  • @ActiveProfiles("test") 등의 프로파일이 설정도 가능하다.

4.1.1 JPA 관련 테스트 설정만 로드

  • @SpringBootTest : 애플리케이션의 전체 클래스를 로드
  • @DataJpaTest : JPA 관련 테스트 설정에 관련된 클래스만 로드
  • JpaRepository를 상속하는 클래스(repository)는 @DataJpaTest에서 알아서 올려주기 때문에 import를 통해 직접 주입하지 않아도 됨.
  • JPA 관련 테스트 설정이 아닌 경우, 직접 스프링 컨테이너에 클래스를 로드시켜줘야함.
    • 테스트 대상 클래스가 의존하는 것이 인터페이스라면 @TestConfiguration 사용해서 안에서 @Bean을 만들고 직접 주입해줘야 함.
    • 테스트 대상 클래스가 의존하는 것이 클래스라면 @Import 어노테이션을 통해 컨테이너에 로드해줘야 함.

4.1.2 @Transactional

  • 내부에 @Transactional를 포함해 테스트 진행 후 데이터를 롤백함.
  • @Transactional 기능 비활성화 :
    • @Transactional(propagation = Propagation.NOT_SUPPORTED)

4.1.3 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

  • Replace.Any : 기본적으로 내장된 임베디드 데이터베이스를 사용
  • Replace.NONE :
    • yml 파일에 사용자가 지정한 데이터베이스를 사용하도록 함. (실제 DB)
    • @ActiveProfiles에 설정한 프로파일(yml) 환경값에 따라 데이터 소스가 적용
    • yml에 spring.test.database.replace: NONE와 동일

4.1.4 @AutoConfigureTestDatabase(connection = H2)

  • 특정 테스트 데이터베이스를 사용할 것인 선택이 가능
  • yml 파일의 spring.test.database.connection: H2 설정과 동일함.
  • 기본 설정 : H2 데이터 베이스를 사용함.

4.1.5 TestEntityManager

  • @DataJpaTest에서 EntityManager의 대체제로 만들어진 테스트용 EntitiyManager
  • persist, flush, find 등과 같은 기본적인 JPA테스트가 가능함

4.2 @DataRedisTest

4.2.1 Redis 관련 빈들을 로드

  • 스프링 애플리케이션 컨텍스트에 필요한 Redis 관련 빈들이 로드됨.
    1. Embedded Redis 서버를 시작하고 Redis 연결을 위한 설정을 로드
    2. @RedisHash로 표시된 엔티티들에 대한 Redis Hash Mapping을 구성
    3. CrudRepository 또는 ReactiveCrudRepository를 구현한 레포지토리 빈들을 생성하고 자동으로 구성
  • RedisConnectionFactory, RedisTemplate, StringRedisTemplate 등을 포함

4.2.2 테스트용 Embedded Redis 사용

  • 테스트를 위해 내장된(Embedded) Redis 서버를 사용.
  • 따라서 별도로 Redis 서버를 설치하거나 실행할 필요 없이 테스트를 진행할 수 있음

4.2.3 Redis 테스트 지원 기능 활성화:

  • @DataRedisTest는 Redis와 관련된 테스트를 수행하기 위한 기능들을 활성화한다.

5. 냉부 프로젝트에 적용



5.1 controller 테스트 (통합 테스트)

  • @SpringBootTest

    • 실제 테스트 (통합 테스트)
    • 자동으로 애플레이션 컨텍스트에 모든 빈을 로드한다. (@Component, @Service, @Repository…)
  • @AutoConfigureMockMvc를 통해 MockMVC를 자동 구성함.

  • @Transactional 사용해서 테스트 후 데이터를 모두 롤백함.

  • 스프링 시큐리티를 사용하고 있지만 @WithMockUser, @WithAnonymousUser, @WithUserDetails 중 아무것도 사용하고 있지 않다.

  • controller 테스트에서 통합 테스트를 진행하는 이유 :

    • 의존성 문제나 빈 주입 단계에서 오류가 발생하면 @SpringBootTest가 있는 모든 테스트 클래스는 오류가 발생하는 문제가 생긴다.
    • 하지만 TestRestTemplate를 사용해서 하는 통합 테스트 또한 의존성 문제나 빈 주입 문제가 발생하면 @SpringBootTest가 있는 모든 테스트 클래스에 오류가 발생하는 것은 똑같다.
    • TestRestTemplate 를 사용해서 테스트하는 것과 controller에서 mockMvc를 이용해서 테스트 하는 것의 차이를 모르겠다. 결국 둘 다 url로 테스트를 하는 것인데 굳이 같은 행동을 2번하는 느낌이 들어서 controller에서 통합 테스트를 진행했다.
    • 이 2가지가 다른 효용을 가지고 특별히 분리해야한다고 깨닫는다면 리팩토링을 진행해야겠다.
  • @WithMockUser, @WithAnonymousUser, @WithUserDetails 사용하지 않는 이유 :

    • 지금은 TestTokenService이라는 테스트 클래스를 만들어서 createToken 을 수행하고 바로 그 토큰을 header에 HttpHeaders.AUTHORIZATION와 함께 담아서 전송하는 방법을 통해 시큐리티 필터를 통과하는 방법으로 사용하고 있다.
    • 향후 리팩토링을 진행할 때 수정해보도록 해야겠다.

5.2 Service 테스트 (슬라이스 테스트)

  • Mockito를 이용
  • @Mock + @InjectMock 사용
  • Service 클래스가 의존하는 port에 스터빙을 하고 모의 테스트를 통해 검증

5.3 Repository 테스트 (슬라이스 테스트)

  1. @DataJpaTest 사용
    1. 데이터 롤백,
    2. JPA 관련 테스트 설정만 로드
    3. 그 외의 다른 설정은 @Import를 이용하고 @TestConfiguration를 통해 직접 Bean 주입
  2. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 사용
    1. yml에 설정된 database를 사용
  3. TestEntityManager 사용
    1. 특정 Repository를 테스트하기 위해 다른 Repository 통해서 테스트 결과를 검증할 수 있지만 다른 Repository를 의존하게 되기 때문에 특정 Repository를 테스트하는 경우 해당 Repository만 독립적으로 사용되도록 되어야함.
    2. 그렇기 때문에 TestEntityManager를 사용해서 테스트 결과를 검증
    3. First 원칙 중 I(Isolated - 고립성)이 깨지게 된다.
  4. @TestDataInit 사용
    1. 테스트를 진행하기 전에 데이터 초기화를 위해 만든 커스텀 어노테이션
    2. @TestDataInit({"/ingredientImage.sql", "/ingredient.sql"})
    3. 각각 테스트를 진행하기 전 @TestDataInit 내의 sql 파일의 SQL 쿼리를 실행 (데이터 초기화)
    4. 각각 테스트가 끝나고 delete.sql 파일의 내의 SQL 쿼리를 실행 (테이블 및 데이터 삭제)
  5. @DataRedisTest 사용
    1. Embedded Redis 서버 생성
    2. @RedisHash로 표시된 엔티티와 그 엔티티와 관련된 RedisRepository도 모두 로드시켜줌.

6. 참고


1. 통합 테스트

2. Controller 테스트

3. Service 테스트

  • no content

4. Repository 테스트

profile
안녕하세요! 신입 개발자 김도현입니다.

0개의 댓글