brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Jul 07. 2019

스프링부트 테스트

스프링 부트 Unit Test 및 Integration Test

테스트코드를 작성하는 일은 정말 중요하다. 하지만, 필자에게 아직도 너무 어려운 일이 바로 테스트 코드를 작성하는 일이다. TDD 를 잘하는 개발자는 필자에게 과외를 좀 해주길 바란다. 


암튼, 이번 글에서는 스프링 부트 환경에서의 테스트 코드에 대해서 정리하였다. 참고로 해당 글은 대부분 공식 레퍼런스를 참고하였다. 레퍼런스를 참고하길 바란다. 

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html


단위테스트는 아래 글을 참고하길 바란다.

https://brunch.co.kr/@springboot/418


공식 레퍼런스를 참고하였지만, 개인적인 의견을 많이 추가하였습니다.
잘못된 내용 또는 다른 의견은 편하게 댓글로 피드백을 해주시길 바랍니다.


1. 스프링부트 테스트


스프링부트 테스트에 대해서 간략하게 알아보자. 


Unit Test 에서의 F.I.R.S.T 원칙


일반적으로 단위테스트 코드를 작성할 때 5가지 원칙을 강조한다. (항상 정답은 아니다.)


F - Fast  (테스트 코드를 실행하는 일은 오래 걸리면 안된다.)

I - Independent (독립적으로 실행이 되어야 한다.)

R - Repeatable (반복 가능해야 한다.)

S - Self Validating (매뉴얼 없이 테스트 코드만 실행해도 성공,실패 여부를 알 수 있어야 한다.)

T - Timely (바로 사용 가능해야 한다. )


자세한 내용은 아래 링크를 참고하길 바란다. 

https://dzone.com/articles/writing-your-first-unit-tests


스프링부트 테스트 디펜던시


스프링부트는 애플리케이션 테스트를 위한 많은 기능을 제공한다. 크게 두 가지 모듈을 제공한다.


spring-boot-test  : 핵심 기능 포함

spring-boot-test-autoconfigure : 테스트를 위한 AutoConfiguration 제공


당연한 얘기지만, 스프링 부트에서 테스트를 위한 스타터를 제공한다. "spring-boot-starter-test" 추가하면 필요한 디펜던시를 자동으로 설정해준다. 

JUnit 4 : The de-facto standard for unit testing Java applications.

Spring Test & Spring Boot Test: Utilities and integration test support for Spring Boot applications.

AssertJ : A fluent assertion library.

Hamcrest : A library of matcher objects (also known as constraints or predicates).

Mockito : A Java mocking framework.

JSONassert : An assertion library for JSON.

JsonPath : XPath for JSON.


Junit 5 지원 여부


스프링부트 2.2.0 에서 공식적으로 Junit 5 를 기본 디펜던시로 설정된다. 물론, 2.1.X 이전 버전에서도 Junit 5를 사용할 수는 있지만, 공식적인 방법은 아니었다. Junit4 디펜던시를 제거하고 Junit5를 추가해주면 사용할 수 있었다. 스프링부트 2.2.0은 아직 M4 버전이고, 조만간 공식 RELEASE 버전이 발표될 것으로 추측된다. 2.2.0.RELEASE 버전이 공식 발표되면 그때 다시 상세하게 다룰 예정이다. 



2. 스프링부트 "Unit Tests"


스프링부트 환경에서 Unit Test 를 작성해보자. 스프링부트에서 제공하는 @SpringBootTest 어노테이션을 사용하지 않는다. 즉, 스프링을 전혀 실행하지 않는다는 얘기다. 필자가 생각하는 단위 테스트는 다른 디펜던시가 전혀 없는 상황에서 실행할 수 있는 테스트를 의미한다. @SpringBootTest 는 스프링을 실행시켜야 하기 때문에 필자의 기준으로는 단위 테스트는 아니다. @SpringBootTest 뿐만 아니라, @WebMvcTest 등의 어노테이션도 필자의 기준으로는 통합 테스트의 범주에 속한다. 단, 보는 시각에 따라서 @WebMvcTest 를 단위 테스트로 보는 시각도 많다. 일부 책에서도 @WebMvcTest 를 단위테스트라고 표현하는 책이 꽤 있다. 맞다 틀리다 를 논하고 싶지는 않지만, 이 글에서는 스프링을 전혀 실행하지 않는 테스트일 때만 단위 테스트라고 표현하겠다. 나중에 다시 얘기하겠지만 @WebMvcTest, @DataJpaTest 등의 기능은 Slice Test라는 표현을 사용할 예정이다. 


준비


커피를 조회하는 Service와 Repository 가 있다고 가정해보자. 

해당 코드를 테스트하기 위해서는, 아주 사소한 문제가 있다. 필자가 작성한 CoffeeService에서는 CoffeeRepository를 필드 인젝션으로 Bean을 주입받고 있다. 다들 알겠지만, DI 방법 중 Field 인젝션은 추천하지 않는 방법이다. Pivotal 및 심지어는 IntelliJ에서도 추천하지 않는다. 암튼, 이런 경우에는 스프링을 실행하지 않고 단위 테스트를 수행하는데 문제가 발생한다. 스프링 실행 없이 CoffeeService 클래스는 CoffeeRepository를 주입받을 수 없기 때문이다. 그래서, 스프링 없이 Unit Test를 수행할 수 없다. 조금 머리 아프지만, CoffeeService를 살짝 수정해보자. CoffeeRepository 주입 방법을 필드 인젝션에서 생성자 주입 방법으로 변경하겠다. 

참고로, 생성자 주입을 주로 사용하는 경우에 Lombok을 사용하면 코드를 깔끔하게 줄일 수 있다. @RequiredArgsConstructor 어노테이션을 클래스 상단에 선언하면 별도의 생성자 코드를 작성하지 않아도 된다. 이 글에서는 자세한 내용은 생략하겠다. 


테스트를 해보자. 


커피 이름으로 조회를 잘하는지 테스트해보자. 

정상적으로 테스트 코드가 성공하는 것을 확인하였다. 물론, @SpringBootTest 어노테이션을 사용하지 않았기 때문에 그 어떤 스프링 디펜던시가 추가되지 않다. 스프링 관련 로그는 전혀 올라가지 않기 때문에 콘솔 로그도 아주 심플하다. 근데, 필자의 Repository 코드에서 데이터베이스 연동 로직이 있다면 어떻게 될까? 대부분의 애플리케이션에서는 RDBMS 또는 NoSQL을 연동할 것이다. 이런 경우에 해당 코드는 테스트가 실패한다. 이유는 스프링 실행 없이 실행했기 때문에 그 어떤 Bean 도 생성되지 않았기 때문이다. 예를 들어서, Spring Data JPA를 사용하고, JPARepository를 구현하는 CoffeRepository를 정의했다고 가정해보자. 만약, 스프링 기반으로 테스트 코드를 실행했다면 JPA 연동 설정이 자동으로 실행되고, DataSource를 생성하고, Repository를 Bean(빈)으로 등록되기 때문에 정상적으로 테스트가 성공했을 것이다. 하지만, 이 경우에는 스프링 기반이 아니기 때문에 그 어떤 Bean 도 생성되지 않았다. 


Mock 객체를 사용해서 테스트를 해보자. 


Mockito를 사용해서 가짜 객체를 사용하겠다. 

coffeeRepository.findByNam 을 실행하면, 실제로 데이터베이스 연동 로직을 수행하지 않는다. 즉, Mockito 에 선언된 값으로 인터셉트 된다. 

내부 로직을 디버깅해보면, 아래와 같이 Mockito의 MockMethodInterceptor 클래스에서 interceptAbstract 로 이동하는 것을 확인할 수 있다.

MockMethodInterceptor

Mockito 를 사용해서 단위테스트를 성공하였다. 하지만, 단위테스트 만으로 애플리케이션 테스트를 완료할 수 있을까? 단위테스트가 모두 성공하면 애플리케이션은 정말 신뢰성을 얻을 수 있나??


뭔가 조금 찝찝하다. 


스프링부트 "Unit Test" 한계


단위테스트는 정말 중요하지만, 우리는 스프링 프레임워크에 너무 많은 의존을 하고 있다. 스프링에 의존해서 애플리케이션을 개발하는 상황에서는, 부득이하게 통합테스트를 진행할 수밖에 없다. 모든 코드를 "Unit Test" 로 수행하겠다는 생각은 바람직하지 않다. 물론 스프링 프레임워크 자체를 테스트해서는 안되지만, 스프링이 우리의 요구를 잘 충족하고 지원하는지 검증해야 한다. 그래서 우리는 통합테스트를 적절하게 사용해야한다. 통합 테스트는 애플리케이션의 신뢰도를 높일 수 있다. 하지만, 스프링 통합 테스트는 몇 가지 대가를 치러야만 한다. 그중 가장 큰 대가는 바로, 통합 테스트는 너무 느리다는 사실이다. 



"스프링 자체를 테스트해서는 안되지만,
스프링이 우리의 요구를 잘 충족하고 지원하는지 검증해야 한다."



3. 스프링부트 "Integration Tests"


스프링부트 에서의 통합 테스트에 대해서 알아보겠다. 글 초반에도 설명했지만, 스프링을 실행하는 상황이라면 무조건 통합 테스트라는 가정으로 글을 작성하고 있다. @SpringBootTest 어노테이션을 사용하면 통합 테스트이다. 


@SpringBootTest 어노테이션


@SpringBootTest 어노테이션을 사용하면 아주 간편하게 스프링 애플리케이션 테스트를 할 수 있다. 기존에 사용하던 @ContextConfiguration 의 대안으로, @SpringBootTest 를 선언하면 스프링이 실행되고 ApplicationContext 를 생성하여 작동한다. 기본적으로 @RunWith(SpringRunner.class) 와 함께 사용해야 한다. 


@RunWith


JUnit4 기준으로 @SpringBootTest 어노테이션을 사용하기 위해서 반드시 선언해야 한다.


@SpringBootTest


@SpringBootTest 어노테이션을 사용하면 필요한 Bean 을 모두 올려서 테스트를 할 수 있다. 참고로, 별도의 클래스를 지정하지 않으면 전체 애플리케이션을 로드하고 모든 Bean 을 생성한다. 위에서 설명한 테스트코드를 @SpringBootTest 를 사용해보자. 모든 컴포넌트를 가져와서 실행시켜준다.  

@SpringBootTest 어노테이션으로 선언하고, 통합 테스트를 진행하는 방법은 너무 쉽다. 아주 쉽게 컴포넌트를 주입할 수 있기 때문이다. 통합 테스트를 위해서는 굳이 필드 인젝션을 생성자 주입으로 바꿀 필요도 없다. 스프링이 알아서 잘 주입해주기 때문이다. 


@SpringBootTest & @MockBean


@SpringBootTest 어노테이션을 사용하여, 통합 테스트를 진행하면서 Mock객체를 사용해야 하는 경우가 있다. 스프링에서는 @MockBean 어노테이션을 제공해준다. Repository를 목객체로 사용하는 방법에 대해서 아래 샘플 코드를 참고하자. 



@SpringBootTest 한계


해당 애플리케이션에 테스트 코드와는 전혀 상관없는 컴포넌트 로직에 지연 시간을 주도록 하자. 10초의 딜레이 시간을 주었다. 필자는 CoffeeService를 테스트하기를 원한다. 하지만, 해당 지연 코드는 CityService 관련 로직에 포함되어있다. Coffee 관련 테스트를 위해서는 전혀 필요 없는 컴포넌트인 것이다. 

생성자에 10초의 딜레이 시간을 주었기 때문에, 스프링에서 모든 컴포넌트를 빈으로 생성하는 과정에서 10초의 지연시간이 발생한다. 테스트 코드를 실행하면 스프링이 실행되는 것을 확인할 수 있다. 이때 Bean 이 생성될 것이다. 

최종 테스트 코드 실행 시간은 10초 이상이 걸렸다. 이미 수차례 설명했듯이, @SpringBootTest 어노테이션은 기본적으로(클래스를 지정하지 않는다면) 애플리케이션 전체 컴포넌트의 Bean을 모두 생성한다. 그래서, 테스트 수행 시간이 너무 오래 걸린다. 스프링 프레임워크에서 기본적으로 선언한 Bean 도 있고, 개발자가 필요에 의해서 선언하는 Bean도 많을 것이다. 


통합 테스트를 수행하면서 특정 클래스만 테스트할 수 있는 방법이 있을까??


@SpringBootTest 클래스 지정


@SpringBootTest를 사용하면서 특정 클래스 또는 컴포넌트를 테스트하고 싶다면, classes 속성으로 원하는 클래스를 직접 지정해줄 수 있다. 아래 테스트 코드에서는 CoffeeService, SimpleCoffeeRepository 클래스를 지정하였다. 물론, 해당 방법이 완벽한 단위 테스트(Unit Test)를 의미하지는 않는다. 왜냐면, 결국에는 스프링이 실행이 되기 때문이다. 

어쨌든 이렇게 설정하고 테스트를 수행하면 CoffeeService와 SimpleCoffeeRepository  두 개의 Bean 만을 생성한다. 전체 애플리케이션을 실행하는 기존 방법보다 훨씬 빠르게 테스트를 수행할 수 있다. 


@SpringBootTest, WebEnvironment 서블릿 환경에서 작동


@SpringBootTest 어노테이션은 기본적으로 서블릿 서버를 실행하지는 않는다. WebEnvironment 속성을 사용하면 컨테이너가 서블릿 환경에서 작동되도록 할 수 있다. 기본 설정은 NONE이다.(서버 실행 안 함)

NONE : 기본 설정, ApplicationContext 가 실행되지만 서블릿 웹 서버 환경은 제공하지 않는다.

RANDOM_PORT : ServletWebServerApplicationContext를 실행하고 임베디드 서버를 실행하는데 랜덤 포트로 서버를 띄운다. 테스트가 끝나면 바로 포트는 내려간다.

DEFINED_PORT : ServletWebServerApplicationContext를 실행하고 임베디드 서버를 실행하는데 프로퍼티 설정에 의한 포트로 서버를 띄운다. 기본 포트는 8080이다.

MOCK : 임베디드 서버를 실행하지는 않지만, 가짜의 서블릿 환경을 제공한다. 


참고로, MVC를 위한 웹 테스트는 @WebMvcTest 어노테이션을 사용하는 것을 추천한다. @WebMvcTest를 사용하면 좀 더 가볍게 테스트할 수 있다. 관련해서는 다음 장에서 더 자세히 다루겠다. 



실무에서의 @SpringBootTest


실무에서는 @SpringBootTest 어노테이션을 너무 많이 사용하고 있다. 단위테스트를 해도 되는 상황에서도 @SpringBootTest 어노테이션을 사용한다. 바람직하지 않다. 테스트 목적을 잘 정해야 한다. 


통합테스트 인지

단위테스트 인지


통합테스트 라면 @SpringBootTest 어노테이션을 사용하고

그렇지 않다면 @SpringBootTest 어노테이션을 사용하지 말자

통합테스트 중에서도 특정 클래스만 테스트하면 되는 경우에는 @SpringBootTest 어노테이션의 classes 속성에 반드시 클래스를 지정해주도록 하자. 



@ActiveProfile, @ContextConfiguration, @TestConfiguration 등등


해야할 얘기도 많고, 더 공부할 내용이 정말 많지만 오늘은 이정도로 마무리를 하겠다. 추가 내용은 다음 글에서 다루겠다.


통합테스트 vs 단위테스트 비중은 어떻게 하면 될까?


매우 어려운 일이다. 회사마다, 조직마다, 팀마다 전부 다를 것이다. 필자는 결정하기 어려운데, 지인 또는 전문가의 의견을 들어보면 스프링부트 환경에서의 테스는 보통 통합테스트 50%, 단위테스트 50%로 딱 반반 정도로 구성하면 되겠다는 의견이 많았다. 정답은 없다. 각자 상황에 맞게 잘 결정하자. 



글 마무리

이번 글에서는 스프링부트 테스트 코드 작성에 대해서 정리하였다. 다음 글에서는 스프링부트의 "Slice Test" 에 대해서 다룰 예정이다. 스프링에서 제공하는 Slice Test 는 아래와 같다. 


@WebMvcTest

@DataJpaTest

@DataMongoTest

@DataRedisTest 

등등..


일부 서적에서는 단위테스트 라고 표현하기도 한다. 사실, 필자가 생각하기에는 해당 테스트는 모두 스프링을 실행하기 때문에 통합테스트라고 생각한다. 근데, 조금 애매하기는 하다. 그래서 필자의 글에서는 "Slice Test" 라는 표현을 사용할 예정이다. 


다음 글에서는 스프링부트에서 제공하는 다양한 Slice Test 및 각종 지식을 공유하겠다. 

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari