본문 바로가기
Spring

Springboot 단위 테스트

by 지금 느낌 그대로 2020. 12. 31.

Why?


테스트 코드를 왜 작성해야 하는지 검색하면 많은 사람들이 테스트코드의 중요성을 강조하면서 유지보수의 편리함, 코드의 문서화, 시간의 단축, 디버깅의 편리함과 같은 이유를 말한다. 나 같은 경우에는 이 글들을 읽으면서 중요하다고 생각은 하지만 뭔가 가슴에 와닿지는 않는다는 느낌을 지울수가 없었다. 그래서 실제로 프로젝트를 진행할때도 형식적으로 작성하거나 그냥 넘어가는 경우도 많이 있었다.

그러다 springboot 관련 책을 공부하다 테스트 코드에 관한 글을 읽게 되었는데, 여기서 나온 테스트 코드를 작성하는 이유를 자신의 경험을 토대로 설명해주었는데, 나에게는 크게 다가와서 좀 더 테스트코드에 대해 상세하게 공부를 하고 습관을 들여야겠다는 생각이 들었다. 책에서 소개한 작가의 경험은 다음과 같다.

① 코드를 작성하고
② 프로그램(Tomcat)을 실행한 뒤
③ Postman과 같은 API 테스트 도구로 HTTP 요청하고
④ 요청결과를 System.out.println()으로 눈으로 검증합니다.
⑤ 결과가 다르면 다시 프로그램(Tomcat)을 중지하고 코드를 수정합니다

여기서 ②~⑤ 는 매번 코드를 수정할 때마다 반복된다는 부분에서 여태까지 내가 얼마나 많이 톰캣을 재실행시켰는지에 대한 기억이 스쳐갔다. 작가의 말대로 코드로 아주 사소한 부분만 변경되어도 버그가 없는지 확인하고 잘 동작하는 지 확인하기 위해 얼마나 많은 System.out.println() 과 프로젝트 재실행을 눌러야 하는가! 

이 시간도 엄청 잡아먹고 스트레스 받는 일을 안해도 된다면 너무나도 좋은 일이라고 생각이 들어 이 글을 작성하게 되었다. 물론 이 외에도 테스트 코드를 작성함으로써 얻을 수 있는 이점은 굉장히 많지만, 내게는 이 부분이 가장 공감되었다.

 

How?


테스트 코드를 작성하려고 검색을 하면서 찾아보았는데, 내게 부족한 부분이 많아 블로그나 티스토리 같은 곳에서 설명하는 간단한 코드를 가지고는 프로젝트에 적용하기 어려웠다. 

대표적으로, Spring MVC 패턴에서 테스트 코드를 작성할 때, 사용되는 다양한 어노테이션들이나 자주 사용되는 메서드 들의 구체적인 사용법이나 REST API는 어떻게, 어디까지 테스트 코드를 작성해야하는지가 무척 힘들었다.

알고 있는 것들도 많지 않아 어떤 키워드로 검색을 해야하는 지에 대해서도 몰라서 처음에 너무 힘들었던 기억이 있다.

그래서 MVC 패턴에서 자주 사용되는 형식들에 대한 간단한 예제코드를 통해서 어떤식으로 코드를 작성하는지 알아보자.

 

MVC 테스트(컨트롤러 테스트)


SampleController

@RestController
public class SampleController {
	
    @GetMapping("/api/sample") 
    public String sample() {
    	return "hello";
    }

    @GetMapping("/api/sample/dto")
    public SampleDto sampleDto(@RequestParam("id") Long id) {
        return new sampleDto(id);
    }    
}

 

SampleControllerTest

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

    @Autowired
    private MockMvc mvc
     
     @Test
     public void returnStringHello() throws Exception {
         String testString = "hello";

         mvc.perform(get("/api/sample"))
                 .andExpect(status().isOk())
                 .andExpect(content().string(testString));
     }

    @Test
    public void returnSampleDto() throws Exception {
         Long id = 4;

         mvc.perform(get("/api/sample/dto")
                 .param("amount",String.valueOf(id)))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.id",is(id)));


     }
}

위 코드는 RestController에서 string이나 JSON을 리턴하는 경우, 어떤 식으로 테스트 코드가 작성되는지를 보여주는 예제이다.

조금 낯선 어노테이션과 클래스에 대해 알아보면,

 

① @RunWith(SpringRunner.class) 

Java에서 테스트를 지원하는 프레임워크인 JUnit의 실행 방법을 확장할 때 사용하는 어노테이션이다.

기본적으로 SpringJUnit4ClassRunner라는 확장 클래스를 지정해주면 테스트를 진행하는 과정에서 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 담당한다. 위의 코드에서 SpringRunner가 SpringJUnit4ClassRunner의 별칭으로 둘의 차이는 없다. 내 추측으로, 너무 길어서 좀 줄인 버전을 만든게 아닐까?

위의 코드에서는 JUnit4를 사용하고 있는데, 최신버전은 JUnit5이다. JUnit5에서는 RunWith 어노테이션이 사라지고 ExtendWith이라는 새로운 어노테이션이 이를 대체한다고 한다.

 

② @WebMvcTest

컨트롤러만 따로 테스트할 경우 사용되는 어노테이션이다. 테스트를 할 때, 어플리케이션의 모든 컴포넌트들을 빈으로 등록하지 않고 웹과 관련된 클래스. 즉, 컨트롤러에서 관리하는 영역만 Spring Context로 처리하고(Ex. filter, session,cookie) 이후 service 혹은 repository들은 모두 mock이라 불리는 객체로 처리한다. 그래서 이후 연관된 Bean들을 사용하려면 @MockBean 으로 선언해 해당 mock의 가짜객체(표현이 정확한지 잘 모르겠다)를 만들어 줘야 한다.

이 때,  @Autowired를 사용하면 오류가 발생한다.

또한, @SpringBootTest와 같이 사용할 수 없다. 그 이유는, 바로 아래의 McokMvc을 서로 설정하기 때문에 충돌이 발생하기 때문이다.

 

③ MockMvc

웹 애플리케이션을 애플리케이션 서버에 배포하지 않고도 스프링 MVC의 동작을 재현할 수 있는 클래스.

@Test가 선언된 메서드는 DispatcherServlet에 요청을 보내고, test용으로 확장된 TestDispatcherServlet은 요청을 받아 매핑정보(/api/sample, /api/sample/dto)를 보고 그에 맞는 핸들러(컨트롤러) 메서드를 호출하고 테스트케이스 매서드는 MockMvc가 반환하는 실행결과를 받아 실행결과가 맞는지 검증한다. 원래는 MockMvc 객체를 직접 생성하여 설정하거나 @AutoConfigureMockMvc 를 사용해 설정을 해야하지만, @WebMvcTest 를 사용하면 어노테이션이 알아서 설정을 해주므로 따로 선언할 필요가없다. 주로 @SpringBootTest와 함께 MockMvc를 사용할때, @AutoConfigureMockMvc 를 사용한다.

 

④ @Test

Test클래스 안에서 Test를 하려는 메소드 앞에 선언해서 사용한다. 이 어노테이션을 통해서 클래스 단위가아니라 메서드 단위로 테스트를 수행할 수도 있어, 단위테스트의 크기를 줄여 서로간의 의존성을 줄여줄 수 있다.

 

⑤ jsonPath

JSON 응답값을 필드별로 검증할 수 있는 메소드. $를 기준으로 필드명을 적어서 사용한다. 위의 코드에선 id만 검증하므로 $.id로 검증한다.

 

⑥ param()

API를 테스트 할 때 사용될 요청 파라미터를 설정할 수 있는 메소드. String 타입의 값만 허용하므로 Long 타입의 id를 형변환하여 설정하였다.

 

 

위 테스트메서드의 흐름을 살펴보면 다음과 같다.

① MockMvc가 TestDispatcherServlet에 요청
② TestDispatcherServlet은 요청의 매핑정보를 보고 적합한 핸들러 메서드 호출
③ 호출된 핸들러의 메서드가 로직에 따라 값을 반환
④ MockMvc는 반환된 값을 보고 andExpect 메서드를 통해 검증하고 통과하면 테스트 케이스 통과

위 예제를 통해서 간단한 컨트롤러 테스트 (일반적으로 mvc테스트라고 부른다)를 해보았고, 다음으로 컨트롤러 뿐만아니라 spring 통합테스트를 하는 예제를 보자.

 

통합 테스트


@RestController
public class SampleApiController {

    private SampleApiService service;
    
    public SampleApiController(SampleApiService service) {
       this.service = service;
    }
    
   @GetMapping("/api/sample")
    public string hello() {
    
        return service.hello();
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SampleApiControllerTest {
​
   @Autowired
   TestRestTemplate testRestTemplate;
   
   @Test
    public void hello() {
        //given	
        String exptected = "hello";
       
        //when
        String response = testRestTemplate.getForObject("/api/sample", String.class);
       
        //then
        assertThat(response).isEqualTo(exptected);
  } 
}

① @SpringBootTest(webEnvironment = WebEnvironment.RANDOM.PORT)

스프링 부트는 @SpringBootTest를 통해서 스프링부트 어플리케이션 테스트에 필요한 대부분의 의존성을 제공한다.

@SpringBootApplication을 찾아 테스트를 위한 모든 빈들을 다 생성하고, context와 설정 또한 모두 불러와 실제 웹서버에 연결을 시도하는 통합테스트를 하고자 할때 사용한다. webEnvironment 속성을 통해서 환경을 설정할 수 있는데  RANDOM_PORT는 내장 톰캣을 구동하고 실제 가용한 포트를 통해서 응답을 받아 테스트를 수행한다. 즉, MockMvc를 통해서 가짜 객체를 사용할 필요없이 요청과 응답을 테스트할 수 있다. 이 때 자주 사용되는 클래스가 RestTemplate이다.

 

② TestRestTemplate

Spring3 부터 지원된 REST API 호출이후 응답을 받을 때까지 기다리는 동기방식의 REST Client인 RestTemplate를 상속받아 테스트에 적합한 클래스이다. @SpringBootTest에서 WebEnvironment설정을 했다면 TestRestTemplate은 그에 맞춰서 자동으로 설정되어 빈이 생성된다. MockMvc와의 가장 큰 차이점은 Servlet Container를 사용하느냐 안하느냐의 차이다. MockMvc는 Servlet Container를 생성하지 않는 반면, TestRestTemplate은 Servlet Container를 사용한다. 그래서 실제 서버가 동작하는 것처럼 테스트를 수행할 수 있다. 또한 관점의 차이도 존재하는데, MockMvc는 서버 입장에서 구현한 API를 통해 비지니스 로직을 테스트하지만, TestRestTemplate은 클라이언트 입장에서 테스트를 수행한다.

RestTemplate과 관련된 자세한 내용은 하단의 Related에서 링크로 대체한다.

 

③ getForObject()

Http GET 요청을 수행하고 응답을 객체타입으로 변환해서 반환해주는 메서드. 위 코드에서는 컨트롤러 메서드가 String 타입을 반환하므로 String타입으로 인자를 넘겨 반환받고 있다. 이 외에도 Http 메서드와 응답타입에 따른 메소드가 존재한다.

 

 

해당 테스트메서드의 흐름은 RestTemplate의 동작원리와 관련이 있어 이 글에서는 다루지 않고, 하단의 Reference에 링크로 대체하도록 하겠다.

 

Reference


sjh836.tistory.com/141

 

RestTemplate (정의, 특징, URLConnection, HttpClient, 동작원리, 사용법, connection pool 적용)

참조문서 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html 1. RestTemplate이란? spring 3.0 부터 지원한다. 스프링에서 제공하는 http..

sjh836.tistory.com

ict-nroo.tistory.com/96

 

[Spring Boot] 스프링 부트 테스트

백기선 - 스프링 부트 개념과 활용 6-1. 테스트 시작은 일단 spring-boot-starter-test를 추가하는 것 부터 test scope으로 추가 @SpringBootTest @SpringBootTest가 하는 역할은 @SpringBootApplication을 찾아서..

ict-nroo.tistory.com

galid1.tistory.com/783

 

Test - Test 코드를 작성해야하는 이유와, 방법

1. 테스트코드의 중요성 1. Test Code를 왜 작성해야 하는가 우선 테스트코드를 작성하기 전, 우리가 왜 TestCode를 작성해야 하는지를 먼저 알아야, 귀찮은 테스트코드를 꼭 작성하려고 할것 같습니

galid1.tistory.com

 

'Spring' 카테고리의 다른 글

SpringBoot JSON 형태의 날짜타입 LocalDateTime 으로 받기  (0) 2022.11.17
@Configuration  (0) 2021.01.10
DI(Dependency Injection)  (0) 2021.01.03
ORM, 그리고 JPA ( Java Persistent API )  (0) 2020.12.27
Annotation  (0) 2020.12.26