Web API 테스트 코드 작성

oyeon·2021년 1월 28일
0

Web API를 많이 작성하다보면 웹 어플리케이션을 실행하고 브라우저를 열어서 테스트할 URI를 입력하고 다시 코드를 작성하고 웹 어플리케이션을 재시작하고 등을 반복하게 된다.

Web API를 실행하는 시간보다 웹 어플리케이션을 실행하고 종료하는 시간이 더 오래걸리는 상황이 발생한다.

여기에는 다음과 같은 문제점이 있다.

  • 개발자의 수동 테스트
  • 코드를 수정한 후에 서버를 재시작하고 테스트하기

이런 문제를 해결하기 위해 다음과 같은 방법을 사용할 수 있다.

  • JUnit 사용
  • MockMVC 사용

MockMVC와 JUnit을 이용해 Web API를 테스트하는 방법에 대해 공부하자.

MockMVC

그 동안 웹 애플리케이션을 작성한 후, 해당 웹 애플리케이션을 Tomcat이라는 이름의 WAS(Web Application Server)에 배포(deploy)하여 실행을 하였다.
브라우저의 요청은 WAS에게 전달되는 것이고 응답도 WAS에게서 받게 된다.
WAS는 요청을 받은 후, 해당 요청을 처리하는 웹 어플리케이션을 실행한다.

즉, Web API를 테스트한다는 것은 WAS를 실행해야만 된다는 문제가 있다.
이런 문제를 해결하기 위해서 스프링 3.2부터 MockMVC가 추가되었다.
MockMVC는 WAS와 같은 역할을 수행한다.
요청을 받고 응답을 받는 WAS와 같은 역할을 수행하면서 작성한 웹 애플리케이션을 실행한다.
WAS는 실행 시 많은 작업을 수행한다.

반면, MockMVC는 웹 어플리케이션을 실행하기 위한 최소한의 기능만을 가지고 있다.
그렇기 때문에 MockMVC를 이용한 웹 어플리케이션 실행은 상당히 빠르다.
MockMVC를 이용하면 다음과 같은 테스트를 수행할 수 있다.

MockMVC Test 실습

  • GuestbookApiController를 단위 테스트한다는 것은, GuestbookApiController가 사용하는 GuestbookService에 대한 부분은 함께 테스트하지 않는다는 것을 의미한다.
  • 이를 위해 GuestbookService에 대한 Mock 객체를 사용할 것이고 Mokito를 이용해 Mock 객체를 생성할 것이다.
  • 그리고, Controller를 테스트하기 위해 MockMvc를 사용할 것이다.
  • GuestbookApiController를 Test하는 GuestbookApiControllerTest 클래스를 작성하자.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>kr.or.connect</groupId>
	<artifactId>guestbook</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>guestbook Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<spring.version>4.3.5.RELEASE</spring.version>
		<jackson2.version>2.8.6</jackson2.version>
	</properties>

	<dependencies>
		<!-- Test Framework Dependency -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
			<version>1.9.5</version>
		</dependency>

		<!-- spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!-- servlet JSP JSTL -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>javax.servlet.jsp-api</artifactId>
			<version>2.3.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>jstl</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>

		<!-- jackson 라이브러리는 객체를 JSON으로 또는 JSON을 객체로 변환시킬 때 주로 사용 -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>${jackson2.version}</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jdk8</artifactId>
			<version>${jackson2.version}</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.22</version>
		</dependency>

		<!-- spring jdbc & spring driver & connection pool -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<!-- 요청이 올때마다 Connection 객체를 얻는 것이 아닌, 미리 일정 갯수 찍어내서 Connection Pool 로 
			관리 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
			<version>2.1.1</version>
		</dependency>
		<dependency>
			<groupId>com.jayway.jsonpath</groupId>
			<artifactId>json-path</artifactId>
			<version>2.4.0</version>
		</dependency>
	</dependencies>
	<build>
		<finalName>guestbook</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.6.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
package kr.or.connect.guestbook.controller;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import kr.or.connect.guestbook.config.ApplicationConfig;
import kr.or.connect.guestbook.config.WebMvcContextConfiguration;
import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcContextConfiguration.class, ApplicationConfig.class })
public class GuestbookApiControllerTest {
    private MockMvc mockMvc;
    /* @Mock어노테이션을 붙여서 선언된 guestbookService는
     * Mockito에 의해 Mock객체로 생성된다.(Mock : 모조품(가짜))  */
    @Mock
    GuestbookService guestbookService;

    /* @InjectMocks 어노테이션이 붙어 선언된 guestbookApiController는 
     * Mock객체인 GuestbookService를 사용하게 된다.
     * Spring에 의해 주입된 객체를 사용하는 것이 아니라
     * Mockito에 의해 생성된 Mock 객체가 주입되어 객체가 생성된다. */
    @InjectMocks
    public GuestbookApiController guestbookApiController;

    // 테스트 메소드가 실행되기 전에 @Before 어노테이션이 붙은 메소드가 실행된다.
    @Before
    public void createController() {
    	// 현재 객체에서 @Mock이 붙은 필드를 Mock객체로 초기화시킨다.
        MockitoAnnotations.initMocks(this);
        
        /* MockMVC타입의 변수 mockMvc를 초기화 한다. 
         * guestbookApiController를 테스트 하기 위한 MockMvc객체를 생성한다.*/
        mockMvc = MockMvcBuilders.standaloneSetup(guestbookApiController).build();
    }

    // 방명록을 읽는 Web API를 테스트하는 메서드
    @Test
    public void getGuestbooks() throws Exception {
        Guestbook guestbook1 = new Guestbook();
        guestbook1.setId(1L);
        guestbook1.setRegdate(new Date());
        guestbook1.setContent("hello");
        guestbook1.setName("kim");

        // list에 방명록 한 건을 저장
        List<Guestbook> list = Arrays.asList(guestbook1);
        
        // when(Mock객체.Mock객체메소드호출()).thenReturn(Mock객체 메소드가 리턴 할 값)
        /* guestbookService.getGuestbooks(0) 호출되면
         * list 객체가 리턴되도록 설정  */
        when(guestbookService.getGuestbooks(0)).thenReturn(list);
       
        /* MockMvcRequestBuilders를 이용해 MockMvc에게 호출할 URL을 생성
         * get("/guestbooks") : GET 방식으로 /guestbooks 경로를 호출하라는 의미
         * contentType(MediaType.APPLICATION_JSON); : application/json 형식으로 api를 호출
         * 위의 두 가지 정보를 가진 reqBuilder를 생성  */
        RequestBuilder reqBuilder = MockMvcRequestBuilders
        		.get("/guestbooks").contentType(MediaType.APPLICATION_JSON);
        
        /* mockMvc.perform(reqBuilder) : reqBuilder에 해당하는 URL에 대한 요청을 보냈다는 것을 의미
         * andExpect(status().isOk()) : mockMvc에 위해 URL이 실행되고 상태코드값이 200이 나와야 한다는 것을 의미
         * andDo(print()) : 처리 내용 출력
         */
        /* .andExpect(jsonPath("$.name").value("kim")) 과 같은 문장을 사용하여 
         * Json 결과에 "name":"kim"이 있을 경우에만 성공이 될 수 있도록 할 수도 있다.
         * 이 경우 jsonPath에 대한 라이브러리가 pom.xml파일에 추가 되야 한다. */
        mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

        // Guestbook Mock 객체의 getGuestbooks(0)메소드가 호출했다면 검증은 성공한다.
        verify(guestbookService).getGuestbooks(0);
    }

    // 방명록을 삭제하는 Web API를 테스트하는 메서드
    @Test
    public void deleteGuestbook() throws Exception {
        Long id = 1L;

        when(guestbookService.deleteGuestbook(id, "127.0.0.1")).thenReturn(1);

        /* "/guestbooks/id" 경로를 DELETE 방식으로 호출하기 위한
         * 경로 정보를 가지고 있는 reqBuilder객체를 생성   */
        RequestBuilder reqBuilder = MockMvcRequestBuilders
        		.delete("/guestbooks/" + id).contentType(MediaType.APPLICATION_JSON);
        
        // reqBuilder에 해당하는 URL을 호출한 후, 상태 코드가 200일 경우 성공합니다. 그리고 결과를 출력
        mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

        /* guestbookService Mock 객체의 deleteGuestbook(id, "127.0.0.1")
         * 메소드가 Web API가 동작하면서 호출되었다면 성공 */
        verify(guestbookService).deleteGuestbook(id, "127.0.0.1");
    }
}

결과

  • getGuestbooks() 메서드
  • deleteGuestbook() 메서드

reference
https://www.slideshare.net/sbcoba/spring-test-mvc

profile
Enjoy to study

0개의 댓글