테스트를 학습하며

김성혁·2022년 5월 26일
0

무엇을 테스트할 것인가?가 제일 중요한 포인트

스프링 부트에서는 애플리케이션 테스트를 도와주는 다양한 유틸리티와 어노테이션을 제공한다.

테스트는 두 가지 모듈에 의해서 제공된다.

  • spring-boot-test : 핵심 기능을 포함
  • spring-boot-test-autoconfigure : 테스트를 위한 자동 설정을 제공

개발자들의 편의를 위해 spring-boot-starter-test 라는 스타터를 제공

spring-boot-starter-test 에서 제공되는 의존성

  • JUnit 5 : The de-facto standard for unit testing Java applications
  • Spring Test & Spring Boot Tets : Utilities and integration test support for Spring Boot applications
  • AssertJ : A fluent assetion library
  • Hamcrest : A library of matcher objects (also known as constraints or predicates)
  • Mokito : A Java mocking framework
  • JSONassert : An assertion library for JSON
  • JsonPath : XPath for JSON

스프링 부트의 통합 테스트

  • @SpringBootTest
    • 스프링 부트에서는 Spring Boot 기능이 필요할 때 스프링 테스트 표준 @ContextConfiguration 어노테이션의 대안으로 사용할 수 있는 @SpringBootTest 어노테이션을 제공한다.
      • @ContextConfiguration : 통합 테스트를 위한 애플리케이션 컨텍스트를 어떻게 로드하고 설정하는지를 결정하는데 사용되는 클래스 레벨의 메타 데이터들을 정의
      • 통합 테스트에 필요한 빈들을 하나의 문맥 단위로 관리
      • 해당 어노테이션 선언 시 스프링이 실행되고 ApplicationContext를 생성하여 작동한다. 클래스를 따로 지정하지 않는다면 모든 빈을 올려서 테스트한다는 것을 의미.
    • JUnit 4을 사용한다면 @RunWith(SpringRunner.class)와, JUnit 5를 사용한다면 @ExtendWith(SpringExtension.class)와 함께 사용해야 한다.
      • @RunWith, @ExtendWith 모두 JUnit의 테스트 러너(Runner)를 확장하는 방법을 제공 : JUnit 은 JUnit에 내장된 러너 대신 스프링 구현체로 기능 확장 가능
      • (참고) JUnit5를 사용할 때는 @SpringBootTest 어노테이션 속에@ExtendWith(SpringExtension.class)가 이미 메타 애노테이션으로 사용됐기 때문에 @SpringBootTest를 사용하는 코드에 다시 선언하지 않아도 된다.
    • @SpringBootTest 어노테이션의 사용은 기본적으로 서버를 시작시키지 않는다. 이제껏 스프링을 실행시킨다는 의미에 서버를 시작시킨다는 의미가 내포된 것인 줄 알았다. Intellij의 Run을 이용해 애플리케이션을 실행시키면 서버가 실행되었으니까! 스프링을 이용해 물론 서버를 실행시키도록 서버 환경을 구축할 수는 있지만 스프링은 IoC 컨테이너에 등록된 빈들을 관리해주는 역할을 수행하고 객체들 간의 의존 관계를 만들어준다. @SpringBootTest의 webEnvironment 속성을 사용하여 테스트 실행 방법을 세분화할 수 잇다.
      • webEnvironment 속성에는 어떤 것들이 있을까?
        • MOCK(Default) : web ApplicationContext를 로드하고 mock web environment(원래의 서블릿 컨테이너와는 다른 의미)를 제공한다. 내장 서버는 해당 어노테이션을 사용할 때 시작되지 않는다. 클래스 경로에서 웹 환경을 이용할 수 없는 경우, 웹이 아닌 일반 ApplicationContext를 만드는 것으로 투명하게 대체된다. 웹 애플리케이션 mock 기반 테스트를 위해 @AutoConfigureMockMvc 또는 @AutoConfigureWebTestClient와 함께 사용할 수 있다.
        • RANDOM_PORT : WebServerApplicationContext를 로드하고 실제 웹 환경을 제공한다. 내장 서버가 시작되고 랜덤 포트에서 수신한다.
        • DEFINED_PORT : WebServerApplicationContext를 로드하고 실제 웹 환경을 제공한다. 내장 서버가 시작되고 정의된 포트 또는 기본 포트인 8080에서 수신한다.
        • NONE : 스프링 애플리케이션을 사용함으로써 ApplicaitonContext를 로드하지만 어떠한 웹 환경도 제공하지 않는다.

그러면 여기서 통합 테스트란 무엇인가?

  • 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트

테스트에 @Transactional 어노테이션이 있는 경우, 기본적으로 각 테스트 메서드의 끝에서 트랜잭션을 롤백시킨다. 그러나 RANDOM_PORT 또는 DEFINED_PORT와 함께 이 배치를 사용하면 실제 서블릿 환경을 제공하므로 HTTP 클라이언트와 서버가 별도의 스레드에서 실행되고 별도의 트랜잭션이 실행된다. 이 경우에는 서버에서 시작된 어떤 트랜잭션도 롤백되지 않는다.


Mock 환경에서의 테스트

@SpringBootTest는 내장 서버를 실행시키지 않는 대신 web endpoint를 테스트하기 위해 mock 환경을 설정한다. 스프링 MVC를 사용하면, MockMvc 또는 WebTestClient를 사용하여 web endpoint를 테스트할 수 있다.

  • ApplicationContext를 띄우지 않고 web layer에서 동작하는 테스트에만 집중하고 싶다면 @WebMvcTest 어노테이션을 사용

mock이란 무엇인가?

  • 테스트를 수행할 때 테스트를 수행할 모듈과 연결되는 외부 서비스나 모듈을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 흉내내는 가짜 모듈을 작성하여 테스트의 효율성을 높이는데 사용하는 객체이다.
  • 실제 객체를 만들기에는 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려울 경우, 가짜 객체를 만들어 사용한다.

mock 객체는 언제 필요한가?

  • 테스트 작성을 위한 환경 구축이 어려운 경우
  • 테스트가 특정 경우나 순간에 의존적인 경우
  • 테스트 시간이 오래 걸리는 경우
  • 개인 PC의 성능이나 서버의 성능 문제로 오래 걸릴 수 있는 경우 시간을 단축하기 위해서도 사용한다.

mock에 대한 기본적인 분류 개념

  • 테스트 더블(Test Double)
    • 테스트하려는 객체와 연관된 객체를 사용하기가 어렵고 모호할 때 연관된 객체를 대신해 줄 수 있는 객체
    • 테스트 더블을 사용하면서 얻는 이점은?
      • 테스트 대상 코드 격리
      • 테스트 속도 개선
      • 예측 불가능한 실행 요소 제거
      • 특수한 상황 테스트 가능
      • 감춰진 정보를 확인 가능

  • 더미 객체(Dummy Object)

    • 인스턴스화 된 객체가 필요하지만 기능까지는 필요하지 않은 경우에 사용한다.
    • Dummy Obejct의 메서드가 호출되었을 때 정상 동작은 보장하지 않는다.

  • 테스트 스텁(Test Stub)

    • 더미 객체가 실제 동작하는 것처럼 보이게 만들어 놓은 객체이다.
    • 특정 상황을 가정해서 만들어 특정 값을 리턴해 주거나 특정 메시지를 출력해 주는 작업을 한다. (상태 검증을 위해)
      • 특정 상태를 가정해서 하드코딩된 형태이기 때문에 로직에 따른 값의 변경은 테스트 할 수 없다.

  • 테스트 스파이(Test Spy)

    • Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록할 때 사용한다.
    • 테스트 더블로 구현된 객체에 자기 자신이 호출 되었을 때 확인이 필요한 부분을 기록하도록 구현한다.
    • 실제 객체처럼 동작시킬 수도 있고, 필요한 부분에 대해서는 Stub로 만들어서 동작을 지정할 수도 있다.

  • Mock 객체(Mock Obejct)

    • 행위를 검증하기 위해 사용되는 객체를 지칭하며 수동으로 만들 수도 있고 프레임워크를 통해 만들 수도 있다.
    • 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 만들어진 객체
    • Mock 객체는 테스트 더블 하위객체로 써의 좁은 의미와 테스트 더블을 포함한 넓은 의미 2가지로 사용 될 수 있다.

  • 페이크 객체(Fake Object)

    • 여러 상태를 대표할 수 있도록 구현된 객체로 실제 로직이 구현된 것처럼 보이게 한다.
    • 실제로 DB에 접속하지는 않지만 DB에 접속해서 비교할 때와 비슷한 시나리오로 동작하도록 객체 내부에 구현 할 수 있다.
      • 테스트 케이스 작성을 위해서 다른 객체들과의 의존성을 제거하기 위해 사용
      • 페이크 객체를 만들 때 복잡도로 인해서 노력이 많이 들어갈 경우 적절한 수준에서 구현하거나, Mock 프레임워크를 사용

이러한 테스트 더블들을 사용하기 편리하게 구현해놓은 Mocking Framework들이 존재한다.

왜 mock 환경에서 테스트를 수행하는가?

  • 일반적으로 서블릿 컨테이너를 실행하는 테스트 환경보다 빠르다
  • 그러나 mocking이 Spring MVC layer에서 발생하기 때문에 더 낮은 수준의 서블릿 컨테이너 동작에 의존하는 코드는 MockMvc를 사용하여 테스트할 수 없다. 그럼 이 문제를 어떻게 해결하지?
    • 실제 웹 환경을 제공하는 webEnvironment 속성을 사용하여 해결할 수 있다.
    • webTestClient는 live server와 mock environment 두 곳 모두 사용 가능하다.

Mocking과 Spying Beans

테스트를 진행할 때, 때때로 애플리케이션 컨텍스트 내에 있는 특정 컴포넌트에 목을 쳐서 사용할 필요성이 있고, 스프링 부트에는 ApplicationContext 안에 있는 빈을 목을 쳐서 사용할 수 있게 도와주는 Mockito Mock에서 제공하는 @MockBean 어노테이션을 제공한다.

  • @MockBean

    • 기존에 사용되던 스프링 Bean이 아닌 가짜 객체 Mock Bean을 주입한다.
    • Bean의 이름을 지정해야 함. Bean의 이름을 지정하지 않으면, 스프링에서 어떤 빈을 가져와야하는지 알 수 없어 오류가 발생
    • 해당 어노테이션은 테스트 클래스 내의 필드 또는 @Configuration 클래스 및 필드에 직접 사용할 수 있다. @MockBean 어노테이션은 애플리케이션 컨텍스트가 새로 고침되는 동안 실행되는 빈의 행위를 목치는데 사용되어서는 안된다. 이 상황에서는 @Bean 어노테이션을 사용한다.
    • MockBean을 통해 주입된 가짜 객체의 행동을 선언해줘야 함

  • @SpyBean
    - 이미 존재하는 Bean을 SpyBean으로 Wrapping한 형태라고 생각하면 된다.
    - 선언한 코드 외에는 전부 실제 객체의 것을 사용한다. 선언한 코드 외의 부분은 실제 코드를 통해 전개되고 선언한 코드 부분은 특정 동작으로 목을 치고 싶을 경우 사용
    - 테스트의 범위가 너무 커지는 것은 아닌지 고려하고 사용할 필요성이 있다.
    - @MockBean은 given에서 선언한 코드 외에는 전부 사용할 수 없지만, @SpyBean은 given에서 선언한 코드 외에는 전부 실제 객체의 것을 사용한다.

mock을 치는 방법

  • 목을 칠 대상 객체를 선정하고 해당 객체의 행위에 기반한 테스트 예상 결과를 설정한 후, 테스트 대상 메서드를 실행시킨다.

Mockito : A Java mocking framework

단위 테스트를 도와주고 Mock이 필요한 테스트에 직관적으로 사용할 수 있도록 만들어졌다.

  • Mocking
  • 메서드의 행위 검증
  • 테스트 스텁으로 사용

Mock과 MockBean의 차이점

Mcok은 Mock 객체를 테스트를 실행하는 동안 사용할 수 있게 하기 위해 @RunWith(MockitoJUnitRunner.class)와 함게 사용해야 한다.

@MockBean의 경우 ApplicationContext에 존재하는 빈을 MockBean으로 교체해준다. 즉 ApplicationContext를 띄워서 테스트를 진행할 경우 MockBean을 사용

슬라이스 테스트

스프링 부트의 auto-configuration 시스템은 테스트를 위해서는 너무 과할 수 있기 때문에 슬라이스 테이스를 위해 필요한 부분의 설정만을 로드할 필요가 있다. 스프링 부트의 spring-boot-test-autoconfigure 모듈은 슬라이스 테스트를 할 수 있도록 다양한 테스트 어노테이션을 제공한다.

슬라이스 테스트란?

  • 각 계층을 독립적으로 테스트하기 위해 사용하는 테스트
  • 각 계층을 하나의 단위로 보고 하는 단위 테스트

슬라이스 테스트를 하는 이유?

  • 스프링 부트의 auto-configuration 시스템은 테스트를 위해서는 너무 과할 수 있다.
    • @SpringBootTest 어노테이션을 사용해서 통합 테스트를 진행한다면 모든 빈을 로드하는데 드는 시간과 비용이 높다.
  • 통합 테스트의 테스트 단위가 너무 커서 각 레이어 단위 별 테스트가 필요하다.

슬라이스 테스트를 지원하는 어노테이션들

  • @JsonTest : JSON 직렬화와 역직렬화가 예상대로 동작하고 있는지를 테스트하기 위해 사용되는 어노테이션
  • @WebMvcTest : 스프링 MVC 컨트롤러가 예상대로 동작하고 있는지를 테스트하기 위해 사용되는 어노테이션
  • @DataJpaTest : JPA 애플리케이션을 테스트하기 위해 사용되는 어노테이션

이외에도 다양한 어노테이션들이 존재한다.

현재는 컨트롤러에 대한 슬라이스 테스트를 진행할 것이기 때문에 @WebMvcTest 에 대해서 더 알아보고자 한다.

@WebMvcTest

테스트를 위한 인프라 구축을 위해 @Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterHandlerInterceptorWebMvcConfigurerWebMvcRegistrations, HandlerMethodArgumentResolver 등을 스캔하고 이 밖에 테스트를 하는 데 필요하지 않은 컴포넌트들은 빈으로 등록하지 않는다.

  • 대게 @MockBean과 MockMvc를 사용해서 테스트를 시작한다. MockMvc는 전체 HTTP 서버를 시작할 필요 없이 빠르게 테스트할 수 있는 강력한 방법을 제공한다.

MockMvc

  • 자바독에 의하면 MockMvc는 서버 측 Spring MVC 테스트를 지원하는 주요 진입점이라고 설명되어있다.
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
    
    // ...
    
    WebApplicationContext wac = ...;
    
    MockMvc mockMvc = webAppContextSetup(wac).build();
    
    mockMvc.perform(get("/form"))
         .andExpect(status().isOk())
         .andExpect(content().mimeType("text/html"))
         .andExpect(forwardedUrl("/WEB-INF/layouts/main.jsp"));
  • 서블릿 컨테이너의 구동 없이, 시뮬레이션된 MVC 환경에 모의 HTTP 서블릿 요청을 전송하는 기능을 제공하는 유틸리티

0개의 댓글