2021년 08월 01일 TIL

3 minute read

오늘은 스프링부트에서의 테스트에 대해 공부했다.

스프링 부트 테스트

단위테스트의 정의

단위 테스트(Unit Test)는 하나의 모듈을 기준으로 독립적으로 수행되는 가장 작은 단위의 테스트라고 정의되어있다. 여기서의 모듈을 애플리케이션에서 작동하는 하나의 기능 / 메소드로 이해한다면, 로그인 메소드에 대한 독립적인 테스트가 하나의 단위 테스트가 될 수 있다.

또한 FIRST라는 5가지 규칙을 명심하며 코드를 작성할 필요가 있다.

  • Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.

  • Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.

  • Repeatable: 어느 환경에서도 반복 가능해야 한다.

  • Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.

  • Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

이것은 이론이고, 스프링 부트에서의 테스트는 큰 범주로 보면 세 가지 테스트로 나뉜다고 할 수 있습니다.

  • @SpringBootTest 어노테이션을 활용한 통합 테스트
  • @WebMvcTest, @DataJpaTest 등을 활용한 Test
  • 어떠한 디펜던시도 존재하지 않는 상황에서도 실행 가능한 테스트

사실 @WebMvcTest를 단위테스트로 표현할 수 있는가 없는가에 대해서는 여러가지 갑론을박이 존재합니다.

일체의 다른 디펜던시가 전혀 없는 상황에서 실행할 수 있는 테스트만을 단위테스트라고 이야기하기도하며, @WebMvcTest와 같이 하나의 기능(메서드) 만을 테스트 할 수 있는 환경을 만들어 놓고 테스트 하는 것도 단위테스트에 포함된다고 이야기하기도 합니다.

저는 굳이 어느 것이 맞다라고 이야기하거나, 어떤 것이 더 올바른 지를 이야기하고 싶지는 않습니다.

(굳이 따지면, 저는 모든 디펜던시를 제외하고 하는 테스트만이 단위테스트라고 불리울만하다고 생각하긴합니다.)

주의사항

  • 스프링을 실행하지 않는 단위테스트를 하고 싶다면, DI 중 필드 주입은 존재해선 안됩니다. (스프링의 실행 없이 의존성 주입이 불가능하므로)

  • @SpringBootTest를 사용할 경우, 정말 믿기지 않을 정도로 시간이 오래 걸리고 무겁습니다(서버 올리는데 천만년걸려요)

  • 아무런 연관관계가 없는 단위테스트의 예시

    public class SsafyServiceUnitTest{
        private SsafyRepository ssafyRepository;
        private SsafyService ssafyService;
          
        @Before
        public void setUp(){
            ssafyRepository = new SsafyRepository();
            ssafyRepository.add(new SsafyMember("김주현"));
            ssafyService = new SsafyService();
        }
          
        @Test
        public void ssafy멤버조회성공(){
            SsafyMember sm = ssafyService.findByName("김주현");
            assertEquals("김주현", sm.getName());
        }
    }
    

    그러나 이렇게 테스트를 할 경우, 데이터베이스 연동 로직이 있는 경우 에러가 발생합니다. 따라서 Mock객체를 사용해야합니다.

    public class SsafyServiceUnitTest{
         
          
        @Test
        public void ssafy멤버조회성공(){
            //given
            SsafyRepository ssafyRepo = Mockito.mock(SsafyRepository.class);
            Mockito.when(ssafyRepo.findByName("김주현")).thenReturn(new SsafyMember("김주현"));
            ssafyService = new SsafyService(ssafyRepo);
            //when
            SsafyMember sm = ssafyService.findByName("김주현");
              
            //then
            assertEquals("김주현", sm.getName());
        }
    }
    

    이 경우 Mockito 내부의 인터셉터 클래스에서 interceptAbstract로 넘어가 올바른 결과를 돌려줍니다.

Given-When-Then pattern

@DisplayName("테스트 이름") 
@Test 
void Test() {    
    // given    
    
    // when    
    
    // then 
}
  • @Test 해당 메소드가 단위 테스트임을 명시
  • @DisplayName으로 이름 부여
  • 라이브 템플릿으로 저장해서 빠르게 작성합시다.

Given

테스트를 위해 준비하는 과정. 테스트에 사용하는 변수, 입력 값 등에 대한 정의 / Mock 객체를 정의하는 구문도 포함된다.

When

실제로 액션을 하는 테스트를 실행하는 구간

Then

테스트를 검증하는 과정. 예상한 값, 실행 값 등을 검증

알아두어야 하는 어노테이션

@SpringBootTest

  • 통합 테스트를 위한 어노테이션. @Extention과 함께 사용해야한다.(@RunWith는 JUnit4에서 사용하던 어노테이션입니다.)

  • 기본적으로 @ExtendWith(SpringExtension.class)를 내장하고 있으며, 필요에 따라서 커스터마이징해야할 때 @ExtendWith와 함께 사용한다.

  • Spring Security 환경에서 사용시 @AutoConfigureMockMvc 어노테이션과 함께 사용합니다

  • //내장 메타 어노테이션
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @BootstrapWith(SpringBootTestContextBootstrapper.class)
    @ExtendWith({SpringExtension.class})
    

@ExtendWith

  • 테스트에 필요한 것을 커스터마이징하기 위한 어노테이션. @ExtendWith(MockitoExtension.class)- Service 테스트 시 사용, @ExtendWith(RestDocumentationExtension.class) 등 다양한 확장을 제공한다. 필요한 것을 사용하시면 됩니다.

@Mock

  • Mock 객체를 만들어 반환해주는 어노테이션

@Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션

  • 주로 Encrytion 메소드 같은 것들을 이 어노테이션을 붙여서 씁니다.

@InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션

  • Mock 객체를 주입해서 사용할 수 있도록 만든 객체
  • 예를들어, userService를 테스트한다고 가정하면, userRepo를 Mock객체로 만든 이후 userService에 @InjectMocks를 붙이면 자동으로 Mock객체가 주입된다.

@BeforeEach

  • 테스트 시작 전 가장 먼저 실행하는 어노테이션
  • init 용도로 많이 사용

@MockBean && @SpyBean

  • 이건 Mockito에서 제공하는 어노테이션이 아니라 Spring에서 제공하는 어노테이션이다.
  • 그러나 단위테스트시에는 가능하면 사용을 지양해야하는 어노테이션이다.
  • 그러나 굉장히 비극적이게도, 안쓰는게 생각보다 쉽진 않습니다…
  • 이 사항에 대해 봐두면 좋을 글 : https://jojoldu.tistory.com/320

@WebMvcTest(Controller 테스트 시 사용)

  • @Controller, @RestController가 설정된 클래스들을 찾아 메모리에 생성한다.

  • @Service나 @Repository가 붙은 객체들은 테스트 대상이 아닌 것으로 처리되기 때문에 생성되지 않는다.

  • @WebMvcTest가 설정된 테스트 케이스에서는 서블릿 컨테이너를 모킹한 MockMvc타입의 객체를 목업하여 컨트롤러에 대한 테스트코드를 작성할 수 있다.

  • @WebMvcTest 어노테이션을 사용하면 MVC 관련 설정인 @Controller, @ControllerAdvice, @JsonComponent와 Filter, WebMvcConfigurer, HandlerMethodArgumentResolver만 로드되기 때문에, 실제 구동되는 애플리케이션과 똑같이 컨텍스트를 로드하는 @SpringBootTest 어노테이션보다 가볍게 테스트 할 수 있다.

  • 단, Spring Security 사용 시에 기본 Spring Security 설정을 로드하고, 사용자가 WebSecurityConfigurerAdapter 추상 클래스를 extends 해서 custom 한 클래스를 사용하게 되는 경우, 그에 선언된 url filter를 등록해야만 한다.

    • 이에 따른 해결책은 두가지가 있다. (다른 방법도 있는데, 제가 시도해서 의미 있었던 방법)
      • 첫번째로는, secure = false 옵션을 줘서 Spring Security 인증을 우회하는 방법
      • 두번째로는, excludeFilter를 통해 SecurityConfig filter를 우회하고, @WithMockUser를 통해 가짜 인증 사용자를 생성하여 테스트하는 것이다.

Categories:

Updated: