테스트 코드 작성 표현 방법 (스프링 부트 환경에서)
이번 글에서는, 테스트 코드 작성 시 자주 사용하는 Given-When-Then Pattern에 대해서 간략하게 소개하겠다. 별 내용 없는 글이므로, 아주 편한 마음으로 읽어주길 바란다.
해당 글의 샘플 코드는 자바 & 스프링 부트 환경에서 작성하였다. 혹시라도, 이 글을 읽는 개발자 중에서 스프링 부트 환경에서의 테스트 코드 작성에 대해서 전혀 모른다면, 필자의 예전 글을 먼저 읽어보길 바란다.
https://brunch.co.kr/@springboot/207
해당 글이 잘 이해가 잘 안 된다면,
토비의 스프링을 먼저 읽어보는 것을 추천한다. 오래전 책이지만, 테스트 코드에 대한 좋은 내용이 담겨있다.
단위테스트 작성에 대해서 관심있는 개발자는 아래 글을 읽어보길 바란다.
https://brunch.co.kr/@springboot/418
Given-When-Then 패턴에 대해서 대부분의 개발자가 이미 잘 알고 있으리라 생각이 된다. 필자는 그동안 테스트 코드 작성 시, Given-When-Then 기반의 테스트 코드를 작성하지 않았다. 그다지 중요하게 생각하지 않았고, 글을 쓰고 있는 지금 이 순간에도 반드시 지켜야 하는 규칙이라고는 생각하지 않는다. 하지만, 주니어 개발자 또는 테스트 코드를 처음 작성하는 개발자는 한번쯤은 시도해볼 만한 방법이라고 생각한다.
이 글은, Given-When-Then을 찬양하는 글이 아니며, 필자는 테스트 코드 작성에 아직 서투르다는 점을 이해해주길 바란다. 필자의 내용이 잘못되었다면 피드백을 해주길 바란다.
"Given-When-Then" 은 테스트 코드를 작성하는 표현 방식이다. "마틴 파울러"의 먼저 글을 읽어보라.
https://martinfowler.com/bliki/GivenWhenThen.html
Given-When-Then 은 곧
[준비 - 실행 - 검증] 이다.
아주 심플하지만, 테스트 코드를 작성 시에 준비/실행/검증 의 세 부분으로 나누기만 하면 된다. Given-When-Then 패턴으로 작성한 필자의 코드 샘플은 아래와 같다. 해당 테스트는 "우유가 들어있지 않은 커피의 할인 가격을 검증"하는 테스트 코드이다.
샘플 코드는 커피의 가격을 할인해주는 메서드를 테스트한다. 1000원짜리 아메리카노는 -100원 할인해서 900원을 리턴하는지 검증한다.
테스트를 위해 준비를 하는 과정이다. 테스트에 사용하는 변수, 입력 값 등을 정의하거나, Mock 객체를 정의하는 구문도 Given에 포함한다.
참고로, Mockito를 사용하면서 when이라는 메서드를 사용하였는데, 해당 구문은 테스트를 위한 준비 과정이기 때문에 given에 포함되는 게 맞다.(Mockito의 메서드 이름이 when이라서 조금 헷갈리지만, 어쨌든 given 영역에 포함되어야 한다는 필자의 개인적인 의견...)
실제로 액션을 하는 테스트를 실행하는 과정이다.
할인된 금액을 리턴해주는 getDiscountedPrice 메서드를 실행한다. 테스트는 아주 심플하고 간단하다. 하나의 메서드만 수행하는 것이 바람직하기 때문에, 일반적으로 "When"은 테스트 코드에서 가장 심플한 구문이 될 것이다.
When 은 가장 중요한 구문이지만 가장 짧다. 보통 한 줄이면 끝난다.
마지막은 테스트를 검증하는 과정이다. 예상한 값, 실제 실행을 통해서 나온 값을 검증한다.
Given-When-THen에 대한 내용이 끝이다. 각자 알아서 잘 실천해보길 바란다.
글이 짧게 끝나서 아쉽지만, 이만 마치겠다. 글 쓰는 게 너무 귀찮으니...
글을 짧게 쓰고 마무리하려고 했지만...
아쉬운 마음에 Mock 관련해서 내용을 좀 더 추가하였다. 이어지는 내용도 그냥 재미 삼아 읽어주길 바란다.
Mockito의 @Mock, @InjectMocks에 대해서 알아보자.
또한, 스프링에서 제공하는 @MockBean의 차이에 대해서도 간략하게 알아보겠다.
Service와 Repository 클래스를 아래와 같이 작성한다.
서비스 레이어에 Repository 가 주입된다. Repository 은 실제 데이터베이스에서 데이터를 조회하는 구문이 포함되어 있다. 만약, CoffeeService의 getDiscountedPrice(커피의 할인 가격을 조회하는 기능)를 단위 테스트를 하고 싶은 경우에, CoffeeRepository에 대한 의존성은 어떻게 처리하면 좋을까? 이런 경우 Mock 객체를 생성해서 Repository를 임의로 조작하는 방법을 주로 사용한다. Mocking이라고 부르는 작업이다. 임의의 객체를 생성하기 때문에 실제 데이터베이스에 연동을 하지 않아도 된다.
필자의 글은 쉽게 이해가 잘 안 될 것이다.
필자가 작성한 허접한(?) 테스트 샘플 코드를 같이 보면서 찬찬히 이해해보자.
CoffeeRepository를 상속받는 MockCoffeeRepository 클래스를 테스트 코드 하단에 추가하자.
그리고, 조작이 필요한 메서드를 오버라이드 해서 임의의 테스트 데이터를 정의한다.
참고로, "latte"는 isMilk에 true를 설정하고, "americano" 에는 isMile를 false로 설정한다. Milk 여부에 따라서 커피의 할인 가격이 다른 경우인데, 커피의 할인 가격을 조회하는 getDiscountPrice 메서드는 우유가 포함되어있으면 300원 할인, 없으면 100원을 할인해준다. 그래서, 조작 데이터를 두 가지로 준비하였다.
실제로는 데이터베이스를 통해서 데이터를 조회해야 하지만, 심플한 단위 테스트 작성을 위해서 데이터베이스 조회하는 로직은 생략하고 임의로 제공하는 것이다.
좀 더 쉽게 설명을 해보자. Mock 객체를 사용하지 않는다면 아래와 같이 전체 과정을 모두 연동해야 테스트할 수 있다. "latte"라는 이름의 데이터를 조회하면, 이름이 "latte"이고 가격이 "1400" 원인 데이터가 조회된다. 해당 데이터는 데이터베이스에서 조회한다.
하지만, 해당 테스트의 목적을 다시 생각해보자. 해당 테스트는 데이터베이스에서 데이터 조회를 잘하는지 여부에 대한 테스트가 아니다. 커피 가격의 할인 정책이 잘 적용이 되는지에 대한 테스트라서, 굳이 데이터베이스 연동 여부까지 검증할 필요가 없다. 아래와 같이 임의로 만든 Mock 객체를 통해서 제공하는 원본 데이터를 조작하면 된다.
최종적으로 CoffeeService에 주입되는 Repository는 목객체를 주입해주면 된다.
해당 테스트의 목적을 반드시 기억하자. 1400원짜리 "latte"가 -300 원 할인을 잘하는지에 대한 검증 과정이다. 추가로, 우유가 들어있지 않은 커피는 -100원 할인을 잘하는지 검증해보자.
자.. 테스트 코드는 잘 성공했다.
하지만 뭔가 찝찝하다. 역시나 필자의 코드는 지저분하다. 특히, Mock 객체가 너무 지저분한 것 같다... Mockito를 사용해서 좀 더 깔끔하게 개선해보자.
Mockito를 사용해보자. @Mock 어노테이션을 사용해서 아주 쉽게 목객체를 정의할 수 있다.
그리고, 데이터 조작이 필요한 경우 아래와 같이 Mockito에서 제공하는 메서드를 사용하자.
@InjectMocks 어노테이션을 사용해서 주입할 수도 있다.
@MockBean과 @Mock의 차이에 대해서 잘 모르는 개발자가 많은데, @Mock 은 Mockito 라이브러리에서 제공하는 어노테이션이지만, @MockBean 은 스프링부트에서 제공하는 어노테이션이다. 스프링 부트 환경이면서 통합테스트를 할 때 주로 사용하는 기능이다. 샘플 코드를 보자.
@SpringBOotTest를 사용하면서 MockBean을 정의하면 아주 쉽고 간편하게 목 객체를 정의할 수 있다. 너무 간편해서 놀랍다. 하지만, @SpringBootTest 어노테이션은 별도의 추가 설정을 하지 않는다면 스프링을 실행시키고 애플리케이션에 정의된 모든 Bean을 생성한다. 그래서, 테스트 코드 실행 시 매우 느려질 수 있다. 통합 테스트를 할 때 유용하지만, 단위 테스트에서는 적합하지 않다. SpringBOotTest의 classes 속성에 특정 클래스를 지정해서 해당 클래스만 Bean으로 생성되게 할 수는 있어서 조금 더 빠르게 테스트를 실행할 수는 있지만, 그럼에도 불구하고 스프링을 여전히 실행되기 때문에 느리기는 마찬가지이다. 다른 추가 의견으로는 @MockBean을 남용했을 때 테스트 코드 실행 시 편리함은 있지만, 잘못된 설계를 지나칠 수 있다는 의견도 있다. 관련해서는, 필자가 존경하는 "창천 향로"님의 글을 읽어보길 바란다.
https://jojoldu.tistory.com/320
생략한다. 나중에 각 잡고 다시 글을 쓰겠다.
테스트 코드를 작성하면서 항상 가장 어려운 일은, 테스트 코드 메서드의 네이밍을 작성하는 일이다. 아래와 같은 방법으로 메서드 컨벤션을 정할 수 있다.
MethodName_StateUnderTest_ExpectedBehavior
MethodName_ExpectedBehavior_StateUnderTest
test[Feature being tested]
Feature to be tested
Should_ExpectedBehavior_When_StateUnderTest
When_StateUnderTest_Expect_ExpectedBehavior
Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior
아래 링크를 참고하였다.
https://dzone.com/articles/7-popular-unit-test-naming
필자는 해당 샘플에서 When_StateUnderTest_Expect_ExpectedBehavior 방식으로 정의하였다.
우유가 들어있는 커피는 300원을 할인하는지 검증하는 메서드이다.
네이밍 작성이 너무 어렵다면 한글 주석으로 해당 테스트가 어떤 테스트를 하는지 명시를 해주는 것도 좋은 방법이다. 사실, 한글 주석 없이 무엇을 테스트하려는지 의도를 알 수 있으면 좋겠지만, 아직 테스트 코드 작성에 노하우가 없으니.. 이렇게라도 작성해 놓는 것도 나름 괜찮은 방법이다.
참고로, JUnit 5 에서는 @DisplayName 어노테이션을 별도로 제공하기도 한다.
이 글을 읽는 개발자 중, 필자의 테스트 코드 작성 방법에 조언을 해주고 싶다면 댓글로 코멘트를 남겨주길 바란다.
Given-When-Then에 대한 내용으로 시작한 글이... 마지막에는 테스트 코드 네이밍에 대한 내용으로 끝을 맺었다. 글이, 주저리주저리 산만해졌지만 조금이라도 주변 개발자에게 도움이 되었으면 좋겠다.
https://github.com/sieunkr/spring-boot-test/tree/master/spring-mock-test