Spring

QuickStart 스프링부트_Chapter03_테스트와 로깅, 빌드

강용민 2022. 12. 12. 02:15

1. 스프링 부트 테스트

자신이 작성한 코드에 문제가 없음을 검증하기 위해 어떤 방법으로든 테스트 코드를 작성한다. 그리고 이렇게 작성된 테스트 코드는 유지보수 과정에서 재사용되어 시스템의 안정성을 보장하는 중요한 장치가 된다.

그래서 대부분의 프로젝트에서는 jUnit을 기반으로 일관성 있는 단위 테스트를 진행하는데, 단위 테스트란 자신이 작성한 클래스에 대한 테스트로서 테스트 단계 중에서 가장 기본이라 할 수 있다. 단위 테스트는 테스트할 객체가 최대한 단순해야 한다. 하지만 웹 애플리케이션은 테스트 대상 객체가 특정 서버와 관련 있거나 다른 객체들과 연관되어 관계가 복잡한 경우가 일반적이다.

따라서 서버를 구동하지 않고 컨트롤러만 단독으로 테스트하거나 컨트롤러와 연관된 비즈니스 컴포넌트를 실행하지 않고 컨트롤러만 독립적으로 테스트할 수 있는 환경이 필요하다.

 

1.1 스프링 부트에서 테스트하기

기본으로 제공되는 테스트 케이스 소스는 다음과 같다.

 

Chapter03ApplicationTests.java

package com.rubypaper.chapter03;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
class Chapter03ApplicationTests {

    @Test
    void contextLoads() {
    }

}

먼저 클래스 선언부에 @RunWith가 추가되어 있는데, 이는 JUnit에서 기본으로 제공하는 러너가 아닌 스프링 러너를 사용하기 위해서 추가한 것이다.

더보기

Spring Runner를 사용하는 이유는 만약 기본적으로 제공하는 러너의 경우 application Context를 전부 불러오기에 가벼운 테스트를 하기에는 너무 크다. 하지만 JUnit4에서 지원하는 @RunWith(SpringRunner.class)를 사용하면, @Autowired, @MockBean에 해당되는 것들만 application context를 로딩하므로 사용한다.

@SpringBootTest 어노테이션은 @SpringBootApplication과 비슷하게 테스트 케이스가 실행될 때 테스트에 필요한 모든 설정과 빈들을 자동으로 초기화하는 역할을 수행한다.

 

@SpringBootTest는 여러 속성을 가질 수 있는데 각 속성의 의미는 다음과 같다.

속성 의미
properties 테스트가 실행되기 전에 테스트에 사용할 프로퍼티들을 'key=value'형태로 추가하거나 properties 파일에 설정된 프로퍼티를 재정의한다.
classes 테스트할 클래스들을 등록한다. 만일 classes 속성을 생략하면 애플리케이션에 정의된 모든 빈을 생성한다.
webEnvironment 애플리케이션이 실행될 때, 웹과 관련된 환경을 설정할 수 있다.

 

1.2 MockMvc 이용해서 컨트롤러 테스트하기

목 객체로 테스트하기

테스트를 위해 실제 객체와 비슷한 모의 객체를 만드는 것을 모킹(MOcking)이라고 하며, 모킹한 객체를 메모리에서 얻어내는 과정을 목업(mock-up)이라고 한다.

객체를 테스트할 때 복잡한 절차가 필요하거나 많은 시간이 소요되는 객체는 자주 테스트하기 어렵다. 또는 웹 애플리케이션의 컨트롤러처럼 WAS나 다른 소프트웨어의 도움이 반드시 필요한 객체도 있을 수 있다. 이런 복잡한 객체는 실제 객체와 비슷한 가짜 객체를 만들어서 테스트에 필요한 기능만 가지도록 모킹을 하면 테스트가 쉬워진다 .그리고 테스트하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면, 의존성을 단절시킬 수 있어서 쉽게 테스트할 수 있다.

웹 애플리케이션에서 컨트롤러를 테스트할 때, 서블릿 컨테이너를 모킹하기 위해서는 @WebMvcTest를 사용하거나 @AutoConfigureMockMvc를 사용하면 된다.

 

@WebMvcTest 사용하기

@WebMvcTest를 사용하여 서블릿 컨테이너를 모킹해보자.

 

BoardControllerTest.java

package com.rubypaper;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHello() throws Exception{
        mockMvc.perform(get("/hello").param("name","둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello : 둘리"))
                .andDo(print());
    }
}

@WebMvcTest는 @Controller, @RestController가 설정된 클래스들을 찾아 메모리에 생성한다. 반면 @Service나 @Repository가 붙은 객체들은 테스트 대상이 아닌 것으로 처리되기 때문에 생성되지않는다.

 

@AutoConfigureMockMvc 사용하기

@SpringBootTest에는 웹 애플리케이션 테스트를 지원하는 webEnvironment 속성이 있다. 이 속성을 생략하면 기본 값으로 WebEnvironment.MOCK이 설정되어 있는데, 이 설정에 의해서 서블릿 컨테이너가 모킹된다. 이 모킹한 객체를 의존성 주입 받으려면 @AutoConfigureMockMvc를 클래스 위에 추가해야 한다.

이전에 사용했던 @WebMvcTest와 가장큰 차이점은 컨트롤러뿐만 아니라 테스트 대상이 아닌 @Service나 @Repository가 붙은 객체들도 모두 메모리에 올린다는 것이다. 따라서 간단하게 컨트롤러만 테스트하기 위해서는 @AutoConfigureMockMvc가 아닌 @WebMvcTest를 사용해야 한다.

 

BoardControllerTest.java

package com.rubypaper;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHello() throws Exception{
        mockMvc.perform(get("/hello").param("name","둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello : 둘리"))
                .andDo(print());
    }
}

 

MockMvc 메소드 이해하기

지금부터 작성한 테스트 케이스를 기준으로 MockMvc가 제공하는 메소드들을 하나씩 확인해보자.

우선 웹 요청에 해당하는 MockMvc의 perform() 메소드는 RequestBuilder 객체를 인자로 받는데, get(), post(), put(), delete() 메소드를 사용할 수 있다.

그리고 이 메소드들은 MockHttpServletRequestBuilder 객체를 리턴하는데, 이 객체에 브라주저가 HTTP 요청 프로토콜에 요청 관련 정보(파라미터, 헤더, 쿠키 등) 를 설정하듯 다양한 정보들을 설정할 수 있다. 마지막으로 perform() 메소드를 이용하여 요청을 전송하면, 그 결과로 ResultAction 객체를 리턴하는데, ResultActions는 응답 결과를 검증할 수 있는 andExpect() 메소드를 제공한다.

결과 검증에서 검증할수 있는 것은 다음고 같다.

  • 응답 상태 코드 검증
  • 뷰/ 리다이렉트 검증
    • 컨트롤러가 리턴하는 뷰를 검증할 때는 view() 메소드를 사용한다. andExpect(view().name("hello"))코드는 컨트롤러가 리턴한 뷰 이름이 "hello" 인지 검증한다.
  • 모델 정보 검증
    • 컨트롤러에서 저장한 모델의 정보들을 검증하고 싶으며 MockMvcResultMatchers.model() 메소드를 사용한다.
    • 메소드는 다음과 같다.
      • attributeExists(String name) : name에 해당하는 데이터가 Model에 포함되어 있는지 검증한다.
      • attribute(String name, Object value) : name에 해당하는 데이터가 value 객체인지 검증한다.
  • 요청/ 응답 전체 메시지 확인하기
    • 실제로 생성된 요청과 응답 메시지를 모두 확인해보고 싶은 경우에는 ResultActions의 andDo(ResultHandler handler) 메소드를 사용하면 된다.

 

내장 톰캣으로 테스트하기

이번에는 정상적으로 서블릿 컨테이너를 구동하고 테스트 결과를 확인해보자. @SpringBootTest에서 webEnvironment 속성 값을 RANDOM_PORT나 DEFINED_PORT로 변경하면 된다.

 

Chapter03ApplicationTests.java

package com.rubypaper;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Chapter03ApplicationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testMethod(){
        String result = restTemplate.getForObject("/hello?name=둘리",String.class);
        assertEquals("Hello : 둘리",result);
    }

}

WebEnvironment가 가지고 있는 상수와 의미는 다음과 같다.

상수 의미
MOCK 모킹된 서블릿 컨테이너를 제공하기 때문에 내장 톰캣이 구동되지 않는다. @AutoConfigureMockMvc 어노테이션을 사용하여 MockMvc 객체를 주입 받아 테스트에 사용할 수 있다.
RANDOM_PORT 랜덤한 포트로 내장 톰캣을 구동하여 서블릿 컨테이너를 초기화한다. 정상적인 서블릿 테스트가 가능하다.
DEFINED_PORT RANDOM_PORT와 동일하지만, application.properties 파일에 설정된 서버 포트를 사용한다.
NONE 서블릿 기반의 환경 자체를 구성하지 않는다.

webEnvironment 속성 값이 RANDOM_PORT로 지정하면 더 이상 MockMvc 객체를 목업할 수 없다. 따라서 실제 컨트롤러를 실행해줄 TestRestTemplate 객체를 주입해서 컨트롤러를 요청해야 한다.TestRestTemplate 객체를 이용하면 특정 URL로 서버에 요청을 전달할 수 있으며 응답 결과도 검증할 수 있다.

 

1.3 서비스 계층을 연동하는 컨트롤러 테스트하기

비즈니스 컴포넌트 모킹하기

현재 진행 중인 테스트는 RestTemplate을 이용하여 컨트롤러를 요청했을 때, 컨트롤러는 서비스 인터페이스를 이용하여 비즈니스 컴포넌트의 메소드까지 호출을 전달한다. 하지만 만약 비즈니스 컴포넌트를 생성하는데 리소스가 많이 필요하거나 미완성인경우에는 문제가 생긴다. 스프링에서는 이런 문제를 해결하기 위해서 비즈니스 컴포넌트를 모킹해서 테스트하는 방법을 제공한다.

 

BoardControllerTest.java

package com.rubypaper;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BoardService boardService;

    @Test
    public void testHello() throws Exception{
        when(boardService.hello("둘리")).thenReturn("Hello : 둘리");
        
        mockMvc.perform(get("/hello").param("name","둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello : 둘리"))
                .andDo(print());
    }
}

전과 달라진점은 @MockBean을 이용해서 BoardService 타입의 객체를 사용하고 있다는 점이다. @MockBean은 특정 타입의 객체를 모킹할 수 있기 때문에 비즈니스 객체를 생성하지 않고도 테스트 케이스를 작성할 수 있다.

 

3. 독립적으로 실행 가능한 JAR

개발 완료된 프로젝트가 일반 자바 프로젝트면 간단하게 JAR파일로 패키징하면 되지만 웹 프로젝트라면 WAR파일로 패키징해야 한다. 그리고 단순한 JAR와 달리 WAR의 경우에는 내부 디렉터리 구조와 복잡한 파일들의 위치도 신경 써야한다.

스프링 부트는 웹 애플리케이션도 JAR파일로 패키징하여 사용할 수 있도록 지원하는데 패키징된 JAR 파일은 어떻게 구성되어 있는지 확인해보도록 한다.

 

3.1 스프링 부트 빌드 이해하기

프로젝트 패키징하기

현재 우리가 사용하고 있는 메이븐은 빌드 작업을 자도으로 처리할 수 있도록 빌드 프로세스를 내장하고 있다. 메이븐이 프로젝트를 빌드할 때 기본적으로 사용하는 폴더가 target이다. 

Maven Install을 통해 target 폴더에 SNAPSHOT.jar 파일을 생성할 수 있다.해당 파일을 살펴보자.

BOOT-INF 폴더는 classes와 lib 폴더로 구성되어 있다.

  • classes 폴더 : src/main/java에서 컴파일한 클래스 파일들과 src/main/resources에 작성한 여러 설정 파일들이 모두 포함되어 있다.
  • ib 폴더 : 프로젝트의 [Maven Dependencies]에 등록된 모든 라이브러리들이 포함되어 있는 것을 확인할 수 있다. 심지어 톰캣 서버 라이브러리도 포함되어 있다.

JAR 파일에는 애플리케이션의 메타데이터가 저장된 매니페스트 파일이 있어야 한다. 그리고 그 위치는 META-INF 폴더로 정해져 있다. 

MANIFEST.MF 파일에 대해서 알아보자.

  • Main-Class : jar 애플리케이션 실행을 위한 메인 클래스가 JarLauncher 클래스이다.
  • Start-Class : Chpater03Application  클래스를 시작으로 애플리케이션을 실행한다는 의미이다.
  • Spring-Boot-Classes
  • Spring-Boot-Lib

스프링 부트는 이 메타데이터 파일을 기반으로 애플리케이션 수행에 필요한 다양한 정보들을 해석하고 처리하는 것이다.