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

600g (Kim Dong Geun)·2020년 9월 8일
0

본 글은 나중에 내가 볼려고 정리한 Document 입니다.

본 포스팅은 스프링 부트 Quick Start를 참고하여 작성했습니다 📖

스프링 부트 테스트

개발자들은 누구나 자신이 작성한 코드에 문제가 없음을 검증하기 위해 어떤 방법으로든 테스트 코드를 작성한다.

그리고 테스트 코드는 유지보수 과정에서 재사용되어 시스템의 안정성을 보장하는 중요한 장치가 된다.

자바 계열에서는 Junit을 기반으로 일관성 있는 단위 테스트를 보통 진행하곤 한다..
단위 테스트란 자신이 작성한 클래스에 대한 테스트로서 테스트 단계 중에서 가장작은 단위이자 기본이라 할 수 있다.

단위테스트가 효율적으로 이루어지기 위해서는 테스트할 객체가 단순해야 한다.
하지만 웹 어플리케이션은 테스트 대상 객체가 특정 서버와 관련되어 있거나 다른 객체들과 연관되어 관계가 복잡한 경우가 일반적이다.

예를들자면, 개발자가 만든 컨트롤러가 정상적으로 동작하는지 확인하기 위해서는 서블릿 컨테이너가 구동되어야 하고, 브라우저를 통해 요청/응답 결과를 확인해야 한다. 하지만 컨트롤러를 수정할 때마다 매번 브라우저를 통해 결과를 확인하는 것은 매우 번거롭다.

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

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

스프링부트에서 테스트를 사용하기 위해서는 spring-boot-starter-test 라는 의존성이 필요하다.

  • pom.xml
    <depencency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

기본 테스트 클래스 이해하기

스프링 부트는 프로젝트를 생성할 때, src/test/java 소스 폴더에 간단한 테스트 케이스를 제공한다. 기본으로 제공되는 테스트 케이스 소스는 다음과 같다.

  • ApplicationTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class AppliationTest{

    @Test
    public void conextLoads(){
    
    }
    
}

먼저 선언부에 @RunWith이 추가되어 있는데, 이는 Junit에서 기본으로 제공하는 러너가 아닌 스프링 러너를 사용하기 위해서 추가한 것이다. (내가 알기론 스프링4 에서 사용하기 위함이고 5부터는 없어도 되는 것으로 알고있다.)

또한 선언부에 @SpringBootTest 어노테이션이 추가되어있는데, 테스트 케이스가 실행될 때 테스트에 필요한 모든설정과 빈들을 자동으로 초기화하는 역할을 한다.

SpirngBootTest 어노테이션

@SpringBootTest어노테이션은 복잡한 테스트 설정들을 자동으로 처리하고, 테스트 관련 객체들도 메모리에 올리는 일련의 과정들을 자동으로 설정해준다.

@SpringBootTest 속성중에서 properties속성이 중요한데, 이 속성을 이용하면 외부(application.properties)에 설정된 프로퍼티 정보를 재정의하거나 새로운 프로퍼티를 등록하여 사용할 수 있다.

외부 프로퍼티 사용하기

테스트 케이스를 작성하다 보면 여러 테스트에서 공통으로 사용하는 데이터들이 있다. 이런 데이터를 외부에 프로퍼티로 등록하면 테스트 데이터를 재사용하거나 변경하기 쉽다.

프로퍼티 참조하기

application.properties (혹은 yml) 파일이 다음과 같이 설정되어있다

## Test Property Setting
author.name= TESTER
auther.age = 27

그리고 추가된 프로퍼티 정보를 사용하도록 테스트 케이스를 설정한다.

  • ApplicationTest.java

@RunWith(SpringRunner.clasS)
@SpringBootTest
public class ApplicationTest{
    @Autowired
    private Environment environment;
    
    @Test
    public void testMethod(){
        System.out.println("이름 :"+environment.getProperty("author.name"));
        System.out.println("나이 : "+ environment.getProperty("author.age"));
        System.out.println("국가 : "+environment.getProperty("author.nation"));
    }

environment객체를 이용하여 application.properties에 정의된 값들을 불러 올 수 있다.
그러나 프로퍼티 파일에 등록되지 않은 author.nation에 대한 정보는 출력되지 않는다.

프로퍼티 재정의하기

그럼 위에 대한 예제는 왜 준비 했겠는가? 당연히 테스트에선 재정의 할 수 있으니까 했겠지 않겠나
@SpringBootTest를 이용하면 외부 프로퍼티 파일에 등록된 프로퍼티를 재정의할 수도 있고 새로운 프로퍼티를 추가할 수 있다.

다음과 같이 작성해보자

  • ApplicationTest.java
@Runwith(SpringRunner.class)
@SpringBootTest(classes=BoardController.class,
    properties("author.name=kim dong geun",
         "author.age=45",
         "author.nation=South Korea"})
public class ApplicationTest{
    @Autowired
    Environment environment;
    
    @Test
    public void testMethod(){
        System.out.println("이름 :"+environment.getProperty("author.name"));
        System.out.println("나이 : "+ environment.getProperty("author.age"));
        System.out.println("국가 : "+environment.getProperty("author.nation"));
    }
}

추가된 @SpringBootTest 속성에서 classes는 테스트할 클래스를 지정할 때 사용한다.
classes로 지정된 클래스는 컨테이너가 자동으로 메모리에 올린다.
하지만 classes에 등록되지 않은 클래스는 객체를 생성하지 않기 떄문에 불필요한 메모리 낭비를 피할 수 있다.

또한 properties속성을 이용해서 application.properties파일에 설정된 외부 프로퍼티를 재정의 하면서 동시에 author.nation이라는 새로운 프로퍼티도 추가했다.

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

Mock 객체로 테스트하기

목(Mock)이라는 단어를 사전에서 찾아보면 테스트를 위해 만든 모형을 의미한다.
따라서 테스트를 위해 실제 객체와 비슷한 모의 객체를 만드는 것을 모킹이라고 하며,
모킹한 객체를 메모리에서 얻어내는 과정을 목업(Mock up)이라고 한다.

객체를 테스트 하기 위해서는 테스트 대상 객체가 메모리에 있어야 한다. 하지만 생성하는데 복잡한 절차가 필요하거나 많은 시간이 소요되는 객체는 자주 테스트하기 어렵다.

또한 웹 애플리케이션의 컨트롤러처럼 WAS나 다른 소프트웨어의 도움이 반드시 필요한 객체도 있을 수 있다. 이런 복잡한 객체는 당연히 테스트 과정도 복잡하고 어려울 수 밖에 없다.

따라서 테스트 하려는 실제 객체와 비슷한 가짜 객체를 만들어서 테스트에 필요한 기능만 가지도록 모킹을 하면 테스트가 쉬워진다.

즉 모킹한 객체를 이용하면, 의존성을 쉽게 단절시킬 수 있어서 테스트하기 용이하다.

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

여기서 중요한 점 서블릿 컨테이너를 모킹한다는 말은 무슨 말일까?
웹 환경에서 컨트롤러를 테스트하려면 서블릿 컨테이너가 구동되고 DispatcherServlet 객체가 메모리에 올라가야 한다.

하지만 서블릿 컨테이너를 모킹하면 실제 서블릿 컨테이너가 아닌 테스트용 모형 컨테이너를 사용하기 때문에 간단하게 컨트롤러를 테스트할 수 있다.

@WebMvcTest 사용하기

먼저 간단하게 @WebMvcTest를 사용하여 서블릿 컨테이너를 모킹하는 실습을 진행해보자.

  • BoardContollerTest.java
@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이다.

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

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

AutoConfigureMockMvc 사용하기

@WebMvcTest와 비슷하게 사용할 수 있는 어노테이션으로 @AutoConfigureMockMvc가 있다. 먼저 @SpringBootTest에는 웹 어플리케이션 테스트를 지원하는 webEnvironment 속성이 있다. 이 속성을 생략하면 기본 값으로 WebEnvironment.MOCK가 설정되어 있는데, 이설정에 의해서 서블릿 컨테이너가 모킹된다. 즉 테스트 케이스 실행 시에 서블릿 컨테이너를 구동하지 않는 말이다.

그리고 @SpringBootTest(webEnvironment=WebEnvironment.MOCK) 설정으로 모킹한 객체를 의존성 주입 받으려면 @AutoConfigureMockMvc를 클래스에 추가해야 된다.

  • BoardControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=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());
   }
}

이전에 사용했던 @WebMvcTest와 가장 큰 차이점은 @AutoConfigureMockMvc는 컨트롤러뿐만 아니라 테스트 대상이 아닌 @Service나 @Repository가 붙은 객체들을 모두 메모리에 올린다는 것이다.

따라서 컨트롤러만 테스트하기 위해서는 @AutoConfigureMockMvc가 아닌 @WebMvcTest를 사용해야 한다.

또한 @WebMvcTest와 @SpringBootTest를 동시에 사용할 수 없다.
왜냐하면 각자 서로의 MockMvc를 모킹하기 때문에 충돌이 발생하기 때문이다.

MockMvc 이해하기

MockMvc가 제공하는 perform()메소드를 사용하면 마치 브라우저에서 서버에 URL 요청을 하듯 컨트롤러를 실행시킬 수 있다. 그리고 andExpect() 메소드를 이용하면 서버의 응답 결과도 검증할 수 있다

우선 웹 요청에 해당하는 MockMvc의 perform() 메소드는 RequestBuilder객체를 인자로 받는데 RequestBuilder 객체는 MockMvcRequestBuildersd의 정적 메소드를 이용해서 생성한다.

MockMvcRequestBuilders의 메소드들은 GET,POST,PUT,DELETE 요청 방식과 매핑되는 get(),post(),put(),delete() 메소드를 제공한다.

그리고 이 메소드들은 MockHttpServletRequestBuilder 객체를 리턴하는데, 이 객체에 HTTP 요청 프로토콜에 요청관련정보(파라미터, 헤더, 쿠키)를 설정하 듯 다양한 정보를 설정할 수 있다.

마지막으로 perform() 메소드를 이용하여 요청을 전송하면, 그 결과로 ResultActions객체를 리턴하는데, ResultActions는 응답 결과를 검증할 수 있는 andExpect()메소드를 제공한다.
andExpect()가 요구하는 ResultMatcherMockMvcResultMatchers에 정의된 정적 메소드를 통해 생성할 수 있다.

컨트롤러의 동작을 테스트하기 위해서는 요청도 중요하지만 사실 컨트롤러가 어떤 결과를 검증했는지 검증하는 것이 가장 중요하다.

서버의 응답결과는 MockMVcResultMachters 객체의 메소드를 이용하여 검증할 수 있다. 지금부터 MockMVcResultMatchers 객체가 어떤 메소드를 제공하여 각 메소드의 기능은 무엇인지 확인해보자.

응답 상태 코드 검증

MockMvcResultMachtersstatus()메소드는 StatusResultMatchers 객체를 리턴하는데 이 객체를 이용하면 응답 상태 코드를 검증할 수 있다.

메소드설명
isOk()응답 상태 코드가 정상적인 처리애 해당하는 200인지 확인한다.
isNotFound()응답 상태가 404Not Found 인지 확인한다.
isMethodNotAllowed()응답 상태가 메소드 불일치에 해당하는 405인지 확인한다.
isInternalServerError()응답 상태 코드가 예외 발생에 해당하는 500인지 확인한다.
is(int status)몇 번 응답 상태 코드가 설정되어 있는지 확인한다.

뷰/리다이렉트 검증

컨트롤러가 리턴하는 뷰를 검증할 때는 view()메소드를 검증한다. 예를들어, andExpect(view().name("hello)) 코드는 컨트롤러가 리턴한 뷰 이름이 "hello"인지 검증한다.
만약 요청 처리 결과가 리다이렉트 응답이라면 redirectedUrl() 메소드를 사용하면 된다.

즉 다음과 같이 사용할 수 있다
andExpect(redirectedUrl("/index")) => '/index' 화면으로 리다이렉트 했는지를 검증

모델 정보 검증

컨트롤러에서 저장한 모델의 정보들을 검증하고 싶으면 MockMvcResultMachters.model()메소드를 사용한다.

메소드설명
attributeExists(String name)name에 해당하는 데이터가 Model에 포함되어 있는지 검증한다.
attribute(String name,Object value)name에 해당하는 데이터가 value 객체인지 검증한다.

이 외에도 응답 헤더 정보를 검증하거나 응답 결과로 생성되는 쿠키를 검증할 수 있는 메소드들도 제공된다.

요청/응답 전체 메시지 확인하기

마지막으로 MockMvc를 이용해서 테스트를 진행할 때, 실제로 생성된 요청과 응답 메시지를 모두 확인해보고 싶은 경우에는 perform()메소드가 리턴하는 ResultActionsandDo(ResultHandler handler)메소드를 사용하면 된다.

MockMvcResultHandlers.print()메소드는 ResultHandler를 구현한 ConsolePrintingResultHandler 객체를 리턴한다. ConsolePrintingResultHandler를 andDo() 메소드 인자로 넘겨주면 콘솔에 요청/응답과 관련된 정보를 모두 출력한다.

내장 톰캣으로 테스트하기

지금까지는 테스트 케이스에서 MockMvc 객체를 목업해서 테스트했기 때문에 톰캣 서버를 사용하지 않고도 테스트할 수 있었다. 그런데 만약 정상적으로 서블릿 컨테이너를 구동하고 테스트 결과를 확인하고 싶으면 webEnvironment 속성 값을 RANDOM_PORTDEFINED_PORT로 변경하면 된다.

다음은 WebEnvironment가 가지고 있는 상수와 의미를 정리한 것이다.

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

여기서 주의할점은!! webEnvironment 속성 값을 WebEnvironment.RANDOM_PORT로 지정하면 더이상 서블릿 컨테이너를 모킹하지 않기 떄문에 MockMvc 객체를 목업할 수 없다.

따라서 MockMvc 객체 대신 실제 컨트롤러를 실행해줄 TestRestTemplate 객체를 주입해 컨트롤러를 요청해야 한다.

TestRestTemplate 객체를 이용하면 특정 URL로 서버에 요청을 전달할 수 있으며, 응답 결과도 검증할 수 있다. 위 소스에서 getForObject() 메소드의 첫 번째 인자로 서버에 요청할 URL을 지정했고, 두 번째로 응답 결과의 타입 클래스를 지정했다. 그리고 assertEqual() 메소드를 이용하여 응답 결과 메시지를 확인했다.

이제 수정된 테스트 케이스를 실행할 때마다 랜덤한 포트로 톰캣 서버가 구동되고 정상적인 웹 요청/응답의 결과를 테스트할 수 있다.

그런데 테스트 케이스를 실행하기 이전에 반드시 applicaiton.properties에 spring.main.web-application-type 프로퍼티 설정이 servlet으로 되어있는지 반드시 확인해야 한다.

servlet이 아닌 none으로 설정되어 있다면 톰캣 서버가 구동되지 않아 무조건 테스트에 실패 할 것이다.

  • BoardControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class BoardControllerTest{
    @Test
    public void testGetBoard() throws Exceptions {
        BoardVO board = restTemplate.getForObject("/getBoard", Board.class);
        assertEquals("테스터",board.getwriter());
    }
}

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

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

현재 진행중인 테스트는 RestTemplate를 이용하여 컨트롤러를 요청했을 때, 컨트롤러는 서비스 인터페이스를 이용하여 비즈니스 컴포넌트의 메소드까지 호출한다.

하지만 앞에서도 언급햇듯이 비즈니스 컴포넌트를 생성하는데 많은 시간과 자원이 필요하거나, 아직 비즈니스 컴포넌트가 완성되지 않아 인터페이스만 제공되는 경우가 있을 수 있다.

이런 상황에서 비즈니스 컴포넌트까지 연결되는 테스트는 문제가 있을 수 잇다.

스프링에서는 이런 문제를 해결하기 위해서 비즈니스 컴포넌트를 모킹해서 테스트하는 방법을 제공한다.

BoardControllerTest 클래스를 수정한다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=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 은 특정 타입의 객체를 모킹할 수 잇기 때문에 비즈니스 객체(BoardsErviceImpl)를 생성하지 않고도 테스트 케이스를 작성할 수 있다.

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글