본문 바로가기

Spring

Mock을 이용한 Sliced Test - @WebMvcTest, @JdbcTest

Spring Framework와 Sliced Test


스프링에서 테스트를 위해 @SpringBootTest를 이용하곤 합니다. 이 어노테이션을 이용하면 모든 Component를 스캔하여 손쉽게 테스트를 작성할 수 있습니다만, 만약 테스트 하고 싶은 단위가 아주 작은 기능 단위라면, @SpringBootTest를 사용하는 것은 아주 비효율적입니다. 왜 비효율적인지는 @SpringBootTest의 실행 과정에서 알 수 있습니다. @SpringBootTest는 아래와 같은 순서로 실행됩니다.

 

  1. 테스트 클래스의 @SpringBootTest 애노테이션을 분석합니다. 이때, @SpringBootTest는 애플리케이션을 로드하기 위해 필요한 설정 정보를 포함하고 있습니다.
  2. 테스트 클래스에서 사용할 빈을 로드합니다. 이때, @SpringBootTest는 애플리케이션 전체를 로드하기 때문에, 애플리케이션의 모든 빈들이 로드됩니다.
  3. 테스트를 실행합니다. 이때, 애플리케이션 전체가 로드되기 때문에, 테스트 수행 시간이 상대적으로 오래 걸릴 수 있습니다.

즉, SpringBootTest는 단위 테스트보다는 통합 테스트에 적합한 스프링에서 제공하는 어노테이션이며, 작은 규모의 테스트에 있어서는 시간 복잡도 측면에서는 느린 테스트 수행 속도와, 메모리 측면에서는 불필요한 빈을 메모리에 로드해야한다는 비효율이 존재합니다. 

 

따라서, Spring에서 단위 테스트를 위해서는 되도록이면 Spring의 도움을 받지 않고 테스트를 진행하면 효율적입니다. 그러나 실제 어플리케이션을 테스트하기 위해서는 Bean을 등록하거나, 데이터베이스와 연결해 쿼리를 전송하거나 데이터를 불러와야합니다. 이러한 스프링의 도움이 필요한 상황에서, 전체 어플리케이션이 아닌 Spring Bean을 테스트 컨텍스트를 

설정할 수 있는 기능이 바로 Sliced Test입니다. 이를 통해 의존성 주입 및 구성을 자동화하되, Configuration을 커스텀하여 보다 빠르고 격리된 테스트 환경을 구성할 수 있습니다.

 

그렇다면 Spring에서 어떤 Annotation으로 Sliced Test를 지원할까요? 

대표적으로 @JdbcTest와 @WebMvcTest는 Spring Framework에서 제공하는 슬라이스 테스트 어노테이션 중 하나입니다. 이 외에도 JPA 레퍼지토리를 테스트하기 위한 @DataJpaTest 등이 있습니다만, 이번 포스팅에서는 Jdbc와 WebMvcTest에 대해서만 알아보겠습니다.


@JdbcTest


@JdbcTest는 데이터베이스와 관련된 기능을 테스트하기 위해 사용됩니다. 이 어노테이션을 사용하면 Spring Data JPA나 JDBC를 사용하여 데이터베이스 연동 기능을 테스트할 수 있습니다. 테스트를 위해 메모리 데이터베이스를 사용할 수도 있습니다.

 

다음과 같이 Test 디렉토리에서 application.properties에 환경 설정을 아래와 같이 할 수 있습니다.

# In memory H2 DB 연동 설정
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.h2.console.enabled=true

@JdbcTest의 시그니쳐를 살펴보면, 빈을 등록하지 않고 Jdbc와 관련된 기능과 DB 테스트에 필요한 @Transactional 기능을 제공합니다.

 

JDBC Test에서 제공하는 어노테이션들입니다.

테스트 코드를 작성하는 것은 스프링 부트 테스트와 거의 동일합니다. 게시판에서 간단한 글을 게시하는 예를 들어보겠습니다. 

@JdbcTest
class ArticleRepositoryTest {

    private DataSource dataSource;
    ArticleRepository articleRepository;
    JdbcTemplate jdbcTemplate;
    private Article art1;
    private Article art2;

	//인메모리 DB 초기화
    @BeforeEach
    public void setup() {
        dataSource = new DriverManagerDataSource("jdbc:h2:mem:test", "sa", "");
        jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.execute("CREATE TABLE ARTICLES\n"
                + "(\n"
                + "writer VARCHAR(255),\n"
                + "title VARCHAR(255),\n"
                + "contents VARCHAR(500),\n"
                + "id int AUTO_INCREMENT,\n"
                + "creationtime timestamp with time zone,\n"
                + "PRIMARY KEY (id)\n"
                + ");\n"
        );

        art1 = new Article( "poro", "글쓰기는 쉽다.", "알고보면 글쓰기는 어려울지도");
        art2 = new Article( "honux", "코딩은 쉽다.", "쉽다");
        articleRepository = new JdbcArticleRepository(dataSource);
        articleRepository.addArticle(art1);
        articleRepository.addArticle(art2);
    }

    @Test
    @DisplayName("글 타입을 인자로 받아 Repository에 저장한다.")
    public void addArticle() {
        assertAll(() -> assertThat(art1.getWriter()).isEqualTo("poro"),
                () -> assertThat(art1.getTitle()).isEqualTo("글쓰기는 쉽다."),
                () -> assertThat(art1.getContents()).isEqualTo("알고보면 글쓰기는 어려울지도"));
    }
}

@WebMvcTest


@WebMvcTest는 웹 애플리케이션에서 컨트롤러와 관련된 기능을 테스트하기 위해 사용됩니다. 이 어노테이션을 사용하면 Spring MVC 컨트롤러를 테스트할 수 있습니다. 슬라이스 테스트이므로, 애플리케이션 전체가 아닌 웹 관련 빈들만 로드되기 때문에, 보다 빠르고 경량화된 웹 애플리케이션 테스트를 가능하게 합니다. 또한, 테스트 대상이 되는 빈들을 쉽게 추가하거나 제외할 수 있는 기능들이 포함되어 있어, 보다 유연한 테스트 수행이 가능합니다.

 

@WebMvcTest의 주석을 한번 읽어보겠습니다.

위 내용을 요약하면, Spring MVC 테스트는 오직 스프링 MVC 컴포넌트 테스트를 위한 어노테이션입니다. 이 어노테이션을 사용하면 @Controller 빈과 같은 MVC 테스트에 필요한 구성만 적용되며, @Component, @Service, @Repository 빈은 로드되지 않습니다. @WebMvcTest로 어노테이션이 지정된 테스트는 Spring Security와 MockMvc를 자동으로 구성합니다. 이 어노테이션은 @Controller 빈에서 필요한 모든 협력관계를 구성하기 위해 일반적으로 @MockBean 또는 @Import와 함께 사용됩니다.

 

읽어보니 MockBean과 MockMvc라는 키워드가 추가로 등장합니다.

 

MVC 테스트를 위해서는 다른 빈과의 의존성을 분리해야합니다. 간단히 예를 들면, 게시판에서 게시글을 작성하는 Controller를 생각해보면, DB에 접근하는 객체와 의존하고 있습니다. Controller를 테스트하기 위해서는 이러한 객체와의 의존을 분리할 필요가 있겠죠. 이를 위해서 필요한 개념이 Mock 객체(MockBean)입니다. Mock은 객체를 흉내내는 모의 객체를 만들어, 모의 객체가 실행할 행동을 주입하고 테스트 대상 객체의 동작을 검증할 수 있습니다. MockBean으로 객체 간의 결합도를 낮추어 좀 더 유연한 단위 테스트를 작성할 수 있을 것입니다.

 

그러나 여기에서 WebMvc의 특성을 고려하면, 한 가지 추가로 필요한 게 있습니다. 바로 Controller에서 필요한 HTTP 요청입니다. MockMvc를 통해서 컨트롤러를 단위 테스트하는 것이 가능해집니다. MockMvc는 Spring MVC 컨트롤러를 테스트하기 위한 Java 기반의 프레임워크입니다. 이를 사용하면 HTTP 요청을 모의하여 Spring MVC 컨트롤러를 테스트할 수 있습니다.

MockMvc는 Spring에서 제공하는 Mock 객체를 사용합니다. Mock 객체는 실제 객체와 유사하지만 동작을 시뮬레이션하는 객체입니다. 이를 통해 서비스, 리포지토리, 데이터베이스 등의 외부 의존성 없이 컨트롤러를 테스트할 수 있습니다. 놀라운 점은 MockMvc는 스프링의 애플리케이션 컨텍스트를 로드하여 웹 애플리케이션과 같은 환경을 모방할 뿐만아니라, HTTP 요청 및 응답을 생성하고, 요청 URL, HTTP 메서드, 요청 파라미터, 쿠키, 세션, 헤더 등의 요청 속성을 설정하거나 검증할 수 있습니다.

 

마찬가지로 예시 코드를 통해서 살펴보겠습니다. Home에서 글 목록을 조회하는 기능을 예시로 보겠습니다. 

 

@ContextConfiguration(classes = TestConfig.class) //원하는 Configuration을 로드할 수 있습니다.
@WebMvcTest(ArticleController.class) //대상 컨트롤러를 지정할 수 있습니다.
class ArticleControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private QnaService qnaService;

    @Test
    @DisplayName("홈에서는 글 목록을 모델에 저장해서 보여준다.")
    public void articleListTest() throws Exception {
        Article article = new Article("poro","제목","내용");
        
        //Mock 객체의 동작 설정
        given(qnaService.lookupAllArticles()).willReturn(Collections.singletonList(article));

		//MockMvc로 HTTP 요청을 모의 테스트
        mvc.perform(get("/"))
                .andExpect(model().attributeExists("article"))
                .andExpect(model().attribute("article", Collections.singletonList(article)))
                .andExpect(view().name("index"))
                .andExpect(status().isOk());

    }
}

 

MockBean을 등록한 후, mockito의 given으로 mock 객체의 메서드가 호출되었을 때 어떤 반환 값을 돌려줄 지 정해주었습니다. 여기에서는 글의 목록을 SingletonList로 반환하도록 mock 객체의 행동을 정해주었습니다.

그리고 MockMvc로 HTTP get요청을 루트 URL로 전송했을 때, response의 view, status, model의 상태에 대해서 검증할 수 있습니다.

 

이상으로 SpringBootTest의 단점을 알아보고, 이를 대신해 Sliced Test의 개념을 살펴보았습니다. Mockito와 Spring의 Sliced Test 어노테이션을 활용한 예제 코드를 보면서 컨트롤러의 단위 테스트 로직을 구현해보았습니다.