brunch

You can make anything
by writing

- C.S.Lewis -

by 에디의 기술블로그 May 31. 2020

주니어 개발자를 위한
단위테스트 샘플 코드 소개

자바&스프링 환경에서 단위 테스트 경험해보기

(2021.06.15) 추가 의견

이 글을 작성한지 1년이란 시간이 지났습니다.. 제가 쓴글을 다시 읽어보니 좋은 글인지 확신이 없습니다. 별로 도움이 안될 것 같습니다.

다른 온라인 강의를 들어보시거나, 더 좋은 자료를 참고해주세요~










글에서는, 자바&스프링부트 환경에서의 단위 테스트 샘플 사례를 설명합니다. 테스트 코드를 처음 접하는 주니어 개발자를 대상으로 작성한 글입니다. 주니어 대상으로 작성한 글이지만, 경험이 많은 개발자분께서 읽어주신다면 너무 감사드리며... 부족한 저에게 작은 조언이라도 해주시길 부탁드립니다.  


글을 시작하기 전에...블로그 포스팅 동기

본격적으로 단위테스트에 대한 글을 시작하기 전에, 글을 작성하게 된 동기에 대해서 소개합니다. 


주니어 개발자분들과 함께한 스터디

주니어 개발자분들과 작은 스터디를 진행 중에, 테스트 코드 작성 방법을 설명하는 것이 너무 어려웠습니다. 말로 설명하지 못한 내용을 글로 작성해서 공유합니다.  


통합테스트 VS 단위테스트

우리는 어떤 테스트를 하고 싶은지 명확하게 알아야 합니다. 이 글에서는, 단위테스트에 대해서 얘기합니다. 스프링부트 환경에서의 통합테스트가 궁금하시다면, 아래 링크를 참고하시길 바랍니다.

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


Given, When, Then

샘플 코드는 given-when-then 형식으로 작성하였습니다. 해당 방식이 정답은 아니지만, 처음 테스트 코드를 접하는 주니어 개발자들에게는 괜찮은 방법입니다. 

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


이 글을 읽어야 하는 대상

자바&스프링 환경에서 테스트 코드를 처음 작성하는 주니어 개발자를 위한 글입니다. 하지만, 경력이 많은 개발자분들께서 봐주시고 저에게 조언을 해주시면 너무 감사할 것 같습니다. 


실패하는 테스트 코드 작성하기

실패하는 테스트 코드를 먼저 작성해보겠습니다.


샘플 코드에 대한 소개

샘플 코드는 외부 오픈 API 를 연동하는 사례입니다. 코드에서는 네이버 오픈 API 를 사용하지만, 카카오API 를 사용해도 상관 없습니다. 네이버 오픈 API 는 네이버 개발자 센터에서 등록이 가능합니다. 

https://developers.naver.com/main/

이 글은 테스트 코드 작성에 대한 글이기 때문에, 오픈 API 연동이 잘되는지는 상관이 없습니다. 


단위테스트는 인터넷이 안되는 상황에서도 실행할 수 있어야 합니다. 

샘플 코드 기본 구조

심플한 스프링부트 애플리케이션 입니다. 스프링 부트 2.2.7.RELEASE 버전이며, 디펜던시는 아래와 같습니다.


기본적인 패키지 구조

패키지 구조는 큰 의미는 없습니다. 아주 심플한 구조로 작성하였습니다. 


요구사항

네이버 오픈 API 는 다양한 검색 API 를 제공합니다. 블로그, 뉴스, 쇼핑, 영화, 책 등 다양한 검색 API를 제공합니다. 이번 글에서는 영화 검색API 사용합니다. 테스트 코드 첫 요구사항은 아래와 같습니다.


- 영화 검색 시 "사용자 평점"이 높은 순으로 정렬해서 API 를 제공한다.


네이버 영화 검색 API의 응답 데이터는 사용자 평점 데이터가 제공되지만, 평점이 높은 순으로 정렬해서 제공하지는 않습니다. 

https://developers.naver.com/docs/search/movie/ 

네이버에서 제공해주지 않기 때문에 어쩔수 없이 평점 순으로 정렬하는 기능은 전체 데이터를 가져온 다음에, 애플리케이션에서 직접 비즈니스 로직을 구현해야 합니다. 


Mock 객체를 만들자

Mock 객체를 왜 만들어야 하나요? 

우리는 MovieService 에 영화 평점 순으로 정렬하는 기능을 구현할 것입니다. 테스트 코드를 작성하기 전에 MovieService 클래스를 먼저 볼게요. 스프링의 @Service 어노테이션이 선언되어있습니다. MovieService 는 스프링부트가 실행될 때 ComponentScan에 의해서 Bean 으로 등록이 될 것입니다.

MovieService 는 Bean(빈)으로 등록이 되는 과정에서, MovieRepository 를 의존성 주입 받습니다. 즉, MovieService 객체를 생성하기 위해서는 반드시 생성자에 MovieRepository가 주입되어야 합니다. 하지만, 생각처럼 쉽지는 않습니다. 이유는, 주입되는 MovieRepository 역시 Bean 으로 등록이 되는 컴포넌트이며, NaverProperties 와 RestTemplate 를 의존성 주입 받기 때문입니다. 

이해가 잘 안되시는 분을 위해서, 아주 허접한 그림을 그려봤습니다.


평점 순 정렬 기능이 구현되는 MovieService 클래스를 만들기 위해서는, MovieRepository 를 만들어서 넣어줘야 하며, 추가로 NaverProperties 와 RestTemplate 까지 만들어서 넣어줘야 합니다. MovieService의 기능을 테스트하기 위해서 너무 많은 객체를 주입해줘야 합니다. 


이런 경우에는 Mock 객체를 사용해야 합니다. 

그림의 오른 쪽 핑크색 영역이 전부 Mocking 이 될 것입니다.

테스트 코드에 MovieRepository 를 상속받는 MockMovieRepository라는 이름의 내장 클래스를 정의합니다. 그리고 MockMovieRepository 클래스로 만든 가짜객체를 MovieService 에 주입합니다. 


평점이 높은순으로 정렬이 잘 되는지를 검증하는 테스트 코드는 아래와 같습니다.

1) given : 테스트 준비 과정

- 테스트를 위해서 필요한 작업을 진행합니다. Mock 객체를 생성해야 하며, 결과가 예상되는 값을 변수로 선언해줍니다. 이 부분은 테스트 코드에서 가장 라인수가 깁니다.


2) when : 테스트 시작

- when 영역은, 테스트 할 메서드를 실제로 실행하며, 라인수가 가장 짧습니다. 일반적으로 한 줄이면 됩니다.


3) then : 테스트 단언

 - 테스트 결과를 검증합니다. 예상했던 값이 나오면 성공입니다. 


테스트 준비 과정(given)에서는 MovieService 를 생성하며 MockRepository 객체를 MovieService 에 주입합니다. MockRepository클래스는 MovieRepository 를 상속받으며, findByQuery 메서드를 Override 합니다. 그리고, 임의로 설정한 데이터를 반환하도록 구현합니다. 

제가 임의로 정의한 데이터에는 첫번째 데이터는 평점이 9.3 이고, 두번째 데이터는 평점이 9.7입니다. 즉, 정렬 기능을 구현하지 않으면 첫번째 데이터는 9.3이 나오기 때문에 평점이 가장 높은 데이터 순서로 제공되지 않습니다. 


실패하는 테스트 코드 작성하기

위에서 작성한 코드는, 정렬 기능이 아직 추가되지 않았기 때문에 실패를 합니다. 테스트코드 좌측 버튼으로 테스트를 실행할 수 있습니다. 

프로젝트 메뉴의 파일에서 우클릭으로 테스트를 선택해서 실행할 수도 있습니다. 

결과는 당연히 실패입니다. 9.7 을 예상했지만, 9.3 이라는 결과가 나왔습니다. 

displayName 이 표시되지 않는다면, 아래와 같이 설정을 해주면 된다.


@Autowired DI(의존성 주입) 사용을 추천하지 않는 이유

만약에...

MovieService에 주입되는 MovieRepository 를 @Autowired 필드 인젝션으로 주입한다면?

테스트 코드 작성 시 MovieRepository 를 주입할 수 없습니다.

이런 경우 해결책은 아직 잘 모르겠습니다.(있다면 제보해주세요.) 어쨋든, @Autowired 를 사용해서 DI 주입을 필드 인젝션 하는 방법은 추천하지 않습니다. 가능하면 생성자 주입을 사용해주세요.


샘플 코드의 테스트 메서드 네이밍이 이상해요

테스트 코드 이름 만드는게 제일 어려워요. 테스트 코드 네이밍에 대해서는 아래 링크를 참고해주세요. 

https://dzone.com/articles/7-popular-unit-test-naming 


샘플 코드

https://github.com/sieunkr/unit-testing/tree/master/test01



1차 요구사항 구현하기

드디어 기능구현을 하고, 테스트 코드를 성공시켜보겠습니다. 


평점이 높은 순으로 정렬하는 기능을 구현한다!!

MovieService 클래스에 findByQueryOrderRating 라는 메서드를 새로 만들었습니다. 해당 메서드는 영화 데이터 리스트를 평점이 높은 순으로 정렬합니다. 그리고, movieRepository 를 사용해서 Repository에서 데이터를 조회하는 로직은, 굳이 외부에 노출될 필요가 없으므로 private 메서드 뒤로 숨겼습니다.

평점 순 정렬 기능은 이걸로 끝입니다. 아주 간단하게 메서드 구현했습니다. 스프링부트 애플리케이션을 실행하면 아래와 같이 브라우저에서 확인할 수 있습니다. 

하지만, 우리는 이번 글에서는 테스트 코드를 통해서 검증해야 합니다. 

이제부터는, 스프링부트 임베디드 톰캣을 실행할 필요가 전혀 없습니다.


Mock 객체를 좀 더 편하게 사용하기 위해서.. Mockito!!!

위 샘플코드에서는 MockMovieRepository 라는 클래스를 테스트코드 내부에 직접 정의해서 Mock 객체를 생성했습니다. 조금 귀찮은 방법이었습니다. 이번에는 Mockito 라이브러리를 사용해서 심플하게 Mock 객체를 만들어 보겠습니다.  

1) @ExtendWith(MockitoExtension.class) : @Mock 어노테이션을 사용하도록 설정합니다.

2) @Mock : Mock 객체를 선언합니다. MovieReopsitory 는 Mock 으로 정의합니다.

3-1)float expectedUserRanking = 9.7;  : 정렬이 되었을 때 가장 먼저 나오는 데이터의 예상되는 평점

3-2)Mockito.when : Mock 객체의 findByQuery 메서드가 실행하면, getStubMovieList() 메서드의 결과를 반환해줍니다. 실제 오픈 API 데이터가 아니라, 테스트코드에서 임의로 정의한 가짜데이터입니다.

3-3)new MovieService(movieRepository) : Mock 객체를 MovieService 에 주입하면서 MovieService 객체를 생성합니다.

4) movieService.findByQueryOrderRating : 테스트를 실행합니다. 


마지막으로

assertEquals(expected... ) : 실행 결과를 검증합니다. 예상했던 값과 같다면 성공입니다. 


가장 중요한 구문은 바로 3-2)Mockito.when 입니다.  Mock 객체의 특정 메서드가 실행했을 때, 임의로 지정해준 값을 (대신)반환해줍니다. 단위테스트에서는 인터넷이 안된다는 가정이며, 네이버 오픈 API 를 사용할 수 없다는 가정입니다. 아래와 같이 임시 데이터를 반환합니다. 

데이터가 복잡하다면, Json 파일을 만들어서 사용하는 것도 괜찮습니다. Json 파일에 데이터를 작성한 후, 테스트 코드에서는 Json 을 불러와서 객체를 생성해주면 됩니다.


테스트 코드를 성공시켜보자.

테스트 코드는 이제 성공해야 합니다. 


리팩토링해도

테스트는 같은 결과가 나와야 합니다.

영화 평점 순으로 정렬하는 구문이 깔끔하지가 않은 것 같습니다. 저는 코드몽키라서 그런지, 제가 작성한 소스코드는 좀 별로인 것 같습니다. 어쨋든, 코드를 수정하고 테스트 코드를 실행해봅니다.

코드를 개선해도, 테스트 결과는 동일하게 같은 결과가 나와야 합니다.  

테스트 코드는 리팩토링을 할 때 많은 도움이 됩니다. 테스트 코드가 전혀 없다면, 리팩토링을 했을 때 기존 기능이 잘 동작하는지 검증하기가 어렵습니다. 

평점순으로 정렬하는 기능은, 

왜 Service 레이어에 구현했나요? 

...라고 질문하신다면, 딱히 제가 할말이 없습니다. 특별한 이유는 없습니다. MovieRepository 에 구현해도 됩니다. MovieRepository 클래스에 구현한다면 findByQueryOrderByUserRatingDesc() 와 같은 이름으로 구현하면 될 것 같습니다. 


해당 샘플 코드는 주니어 개발자를 이해시키기 위해서 어제오늘 급하게 작성한 코드입니다. 

정답이 아니라는 사실을 이해해주시길 바랍니다. 


샘플코드

https://github.com/sieunkr/unit-testing/tree/master/test02



2차 요구사항 구현, 테스트 코드 추가하기

1차 요구사항은 잘 구현하였습니다. 하지만, 보통 회사에서 개발자의 업무는 깔끔하게 끝나지 않습니다. 


'기획자'의 추가 요구사항

기획자는 아래와 같은 요구사항을 추가로 요청합니다.


- 추가 기능1) : 평점 순 정렬 기능에서, 평점이 0인 데이터는 결과에서 제거해주세요.

- 추가 기능2) : 모든 영화 데이터 검색 시, 타이틀(문자열)에 <b>,</b> 태그를 제거해주세요. 


어렵지는 않은 기능입니다만, 기존 기능 역시 잘 동작해야합니다. 

하나 수정했는데, 또 다른 하나가 동작이 안된다면 매우 스트레스 받습니다. (빡칩니다..)


기능 추가를 위한 실패하는 테스트 코드 작성

이번에 사용하는 샘플 데이터는 동일합니다.(앞으로 계속 동일합니다...)


테스트 코드를 어떻게 작성하면 될까요? 쉽게 생각하면 됩니다.


추가 기능1) : 평점 순 정렬 기능에서, 평점이 0인 데이터는 결과에서 제거  

-> 전체 데이터는 4개 입니다. 평점이 0인 데이터는 1개이기 때문에, 결과 데이터의 사이즈가 3이 나오는 것을 검증하면 됩니다.

추가 기능2) : 모든 영화 데이터 검색 시, 타이틀에 들어가는 <b> 태그를 제거해주세요. 

-> 결과 데이터의 타이틀에서 <b> 태그가 제거되었는지 검증하면 됩니다. 


추가 기능1에 대한 테스트 코드는 아래와 같습니다. 

추가 기능2에 대한 테스트 코드는 아래와 같습니다. 

실패하는 테스트 코드를 돌려봅시다. 

기능이 없으니 당연히 실패했습니다. 


테스트가 성공하도록 기능 구현

이때 고민을 해봐야합니다. 


신규 메서드를 만들지 

기존 메서드에 기능을 추가할지 


고민해야 합니다. 

평점이 0인 데이터를 제외하는 기능은 기존 메서드에 추가를 하겠습니다. 이유는, 딱히 없습니다....(?)

특수문자 <b>, </b> 를 제거하는 기능은 신규 메서드로 만들었습니다.

그리고, 해당 메서드는 Repository 에서 데이터를 가져오는 로직에 추가해보겠습니다.


코드가 깔끔하지는 않지만, 빠르게 개발해서 완성해봤습니다. 


테스트는 성공합니다. 또한, 기존 기능(평점 순으로 정렬이 잘 되는지) 역시 잘 동작합니다. 


신규 기능을 추가하거나 리팩토링을 했을 때 

테스트 코드 실행해서, 기존 기능이 잘 동작하는지 체크할 수 있습니다. 



개발자의 실수 (1)

개발자가 코드를 수정하는 과정에서 작은 실수를 하면 어떻게 될까요? 개발자는 항상 바쁘기 때문에 실수를 할 수 있습니다. 실수로 정렬 구문을 주석을 처리했습니다.

바보같은 실수이지만, 종종 발생합니다. 테스트 코드를 실행하면, 아래와 같이 테스트가 실패한 것을 알 수 있습니다. 

신규 기능을 추가하거나, 리팩토링하면 


테스트 코드를 반드시 돌려봐야 합니다. 그리고, 중요한 로직에는 반드시 테스트 코드를 작성해야 합니다.


샘플코드

https://github.com/sieunkr/unit-testing/tree/master/test03


구조를 개선해볼까?

급하게 작성한 샘플 소스 코드가 많이 지저분합니다

주어진 시간이 많지 않지만, 조금이라도 개선해볼까요?


사실, 회사에서도 자주 발생하는 상황입니다. 신규 개발, 운영 업무로 항상 회사에서는 바쁩니다. 하지만, 조금이라도 시간을 내서, 구조를 개선할려고 노력합니다. 코드를 깨끗하게 유지하는 것은 중요합니다. 


참고내용

- 메서드의 매개변수(인수)

함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개(단항)이고, 3개 이상은 가능한 피하는 편이 좋다. [클린코드 50page]

- 응집도

클래스는 인스턴스 변수 수가 작아야 하며, 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.[클린 코드 177page]


구조를 개선해보자.

일단, MovieDTO 클래스를 Movie 라는 이름으로 변경했습니다. 그리고, 특수문자(<b>, </b>)를 제거하는 기능을 Movie 모델에 추가했습니다. 

그리고, Movie 리스트를 갖는 MovieGroup 클래스를 선언합니다. 

이번에 만든 MovieGroup 는 매번 새로운 객체를 만들어서 그때그때 사용하는 객체입니다. 해당 클래스에는 private final 로 선언된 영화 List 컬렉션을 필드로 갖습니다. List를 생성자에서 반드시 주입받아야 합니다. 

MovieRepository 라는 이름의 인터페이스를 선언하고,

MovieRepository 를 구현하는 구현체를 아래와 같이 작성합니다. 

이때 findByQuery 메서드 반환 타입을 변경하였습니다. 

ResponseMovie 에서 List<Movie> 로 변경하였습니다. 


MovieService 에서는 MovieRepository 인터페이스를 주입받습니다. 

실제로는 구현체인 MovieRepositoryImpl 을 주입받습니다. 

MovieService 에서는 아래 샘플 코드와 같이 MovieGroup 객체를 만들어야 합니다.

그리고, 이렇게 개선하고 테스트 코드를 실행하면 테스트가 성공할 것으로 예상했지만,

실패합니다... 


이유는, MovieRepository 의 findByQuery 리턴타입이 변경되었기 때문입니다. 

ResponseMovie 에서 List<Movie> 로 변경되었습니다. 

어쩔수 없이, 테스트 코드를 같이 수정해야 할 것 같습니다. Mock 데이터를 List<Movie>를 리턴하도록 변경해줍니다. 

드디어 테스트는 잘 돌아갑니다. 


리팩토링을 수행할 때 테스트 코드가 잘 작성되어 있으면 리팩토링을 편하게 할 수 있습니다. 리팩토링 후 테스트 코드가 잘 성공한다면, 기존 기능이 잘 동작된다는 것을 알 수 있기 때문입니다. 하지만, 위와 같이 부득이하게 테스트 코드를 같이 수정해야 하는 경우도 발생합니다. 


저는 천재가 아니기 때문에... 한번에 완벽하게 테스트코드를 작성하지는 못합니다. 

설계와 테스트코드는 같이 발전해나가야 합니다. 



신규 기능 추가

평점이 높은 상위 2개 영화의, 평균 평점을 구하는 기능을 추가하고 싶다면?

MovieGroup 클래스에 calculateAverageUserRating() 라는 메서드를 하나 만들어봤습니다. 

단, 메서드는 매개변수를 전혀 받지 않습니다. 클래스에 정의된 변수인 list 를 사용합니다. 


해당 클래스의 대부분의 메서드는 클래스의 인스턴스 변수(list)를 사용하며

메서드는 매개변수(인수)가 전혀 없습니다. 

클래스는 응집도가 높다고 생각하빈다.


제가 생각하는 좋은 클래스입니다.


서비스에서의 호출은 아래와 같이 구현했습니다. 

자 그럼, 테스트 코드를 작성해볼까요? 참고로, 기능 구현하기 전에 테스트 코드를 먼저 작성 안해도 됩니다. 기능 구현한 이후에 테스트 코드를 작성해도 상관 없다고 생각합니다.(개인적인 생각...)

잘 됩네다.  


전체 테스트 코드도 실행해봅니다. 

테스트 코드가 잘 성공하면 마음의 안정이 생깁니다.


개발자의 실수 (2)

기획자와 개발자는 상위 평균 2개의 데이터의 평균만 제공하기로 의사결정을 했었습니다. 시간이 흐른후, 기획자도 퇴사하고, 개발자도 퇴사하고, 문서도 없고... 


히스토리가 전혀 없습니다. 


새로운 개발자가 팀에 합류했고, 새로운 개발자는 아래 TODO 구문을 보고 매우 화가났습니다.

...매직넘버가 있다고 화내면서 해당 코드를 변수로 분리했습니다. 


개발자는 코드를 개선했다고 생각하며 매우 자부심을 느끼고 있습니다. 하지만, 개발자는 실수로 2 를 3으로 잘못 작성한 것을 모르고 있습니다. 테스트 코드가 있다는 사실도 잘 모르고 있습니다. 아무생각없이 서비스 배포를 하였지만, 데이터에 문제가 발생했습니다. 왜냐하면, 해당 데이터는 평점이 높은 상위 2개의 데이터만 평균을 구하는 아주 이상한(?) 로직이기 때문입니다. 회사에서는 이런 이상한(상식밖의) 기능이 비일비재 합니다. 상식선에서 처리하는게 아닙니다. 일반적으로는, 기획자의 요구사항 그대로 구현해야 합니다. 


만약 테스트 코드를 한번이라도 확인했었다면 어땠을까요? 

테스트 코드를 확인했었다면, 자신이 변경한 코드가 잘못된 것을 미리 알 수 있습니다. 


혹시, 

MovieGroup 클래스가... 그 유명한 일급컬렉션인가요?

코드몽키인 저는 잘 모르겠어요. 비슷한것 같습니다. 사실 저는 그동안 일급컬렉션이라는 단어를 회사에서는 사용하지 않아서 잘 몰랐습니다. 

https://jojoldu.tistory.com/412

위 블로그에서 일급 컬렉션에 대해서 잘 설명이 되어있습니다. 


MovieGroupTest 를 만들면

기존 MovieServiceTest 에 있는 테스트는 필요없나요?

MovieGroup 이라는 클래스는 MovieRepository 에 의존하지 않습니다. 테스트 코드를 작성한다면 아래와 같이 작성할 수 있습니다. 

테스트 코드는 정말 간단해졌습니다. Mock 객체를 만들 필요도 없습니다. 너무 좋습니다. 

의존성이 없는 클래스를 만들면 테스트 코드 작성하기에 너무 좋습니다. 


만약, 

"단위테스트"를 작성하기 어렵다면, 해당 클래스가 의존성이 너무 많은 것은 아닌지 의심해야 합니다. 의존성을 줄이면 테스트 코드를 작성하기 쉬워집니다.


1. "단위테스트"를 작성하기 위해 노력하면, 자연스럽게 애플리케이션 설계를 개선할 수 있습니다.

2. 애플리케이션 설계가 깔끔하다면, "단위테스트"를 작성하기 쉽습니다.  



(지극히 개인적인 생각)

그렇다면, MovieServiceTest 에 선언했던 테스트 코드는 삭제해도 될까요? 

하지만, 저는, MovieServiceTest 에 있는 테스트 코드는 삭제 하지 않습니다. 오히려, MovieGroupTest 코드를 작성하지 않을 수도 있습니다. 메서드 자체를 테스트 하는 것이 아니라, 메서드를 통해서 어떻게 동작하는지를 테스트하기 때문에, 굳이 모든 메서드의 테스트 코드를 작성하지는 않습니다. 

(오해가 없기를 바라며, 지극히 개인적인 생각입니다)




샘플코드

https://github.com/sieunkr/unit-testing/tree/master/test05


더 생각해보자.

조금만 더 깊게 생각해볼게요. 


예외 처리에 대한 테스트

예외 상황에 대한 테스트 코드를 작성할 수 있습니다. 잘못된 요청이 들어온 경우에 발생시킬 Exception 을 하나 선언해줍니다. RuntimeException 을 상속받아서 구현합니다. 

그리고, MovieService 클래스의 FindBy 메서드에서 아래와 같이 Exception 처리를 해줍니다. 이때, 필자가 정의한 BadRequestException 을 throw 합니다. 


쿼리에 빈 문자열이 들어오는 경우에는, BadRequestException 를 throw 하는 구문입니다. 즉, 빈 문자열이 들어왔을 때는 애플리케이션에서 예외 처리를 하도록 설정하였습니다. 


테스트 코드 작성은 어떻게 하면 될까요? assertThrows 를 사용해서 아래와 같이 작성할 수 있습니다. 

테스트는 성공합니다.


메서드 당 한개의 테스트 코드를 작성하면 될까?

제 샘플 코드에는 메서드 하나에 두번 테스트한 경우도 있습니다. findByQueryOrderRating 메서드입니다. 


- 평점 순으로 정렬이 잘 되는지

- 평점이 0인 데이터는 제외하는지


두 테스트 모두 movieService 클래스의 findByQueryOrderRating 메서드를 실행해서 테스트 했습니다.

단위테스트는 메서드 자체를 테스트하는 것이 아니라, 메서드가 어떻게 동작하는지를 테스트 하는 것입니다. 즉, 경우에 따라서는 메서드 하나에 다수의 테스트 코드를 작성할 수도 있습니다. 하지만, 주의할 사항이 있습니다. 


한개의 메서드에 너무 많은 테스트 코드가 작성이 된다면, 해당 메서드는 너무 많은 역할을 하고 있다는 신호일 수 있습니다. 


findAndSave() 라는 메서드가 있다고 가정해봅시다. 해당 메서드는 find() 하는 역할과 save() 하는 역할 두개를 합니다. 해당 메서드의 테스트 코드는 최소 두개의 테스트 코드를 작성해야 합니다. 이런 경우에는 find 메서드와 save 메서드를 별도로 분리해야 한다는 신호입니다. 


어쨋든, 메서드(함수) 는 최대한 짧게, 한가지 기능만 하도록 작성하는 것이 좋습니다. 


SpringBootTest 어노테이션을 사용해도 되나요?

당연히 가능합니다. 하지만, 해당 테스트는 단위 테스트가 아닙니다.

 

단위테스트와 어떤 차이가 있을까요? 테스트를 실행하면 스프링부트가 실행이 됩니다. 그리고, 스프링 빈이 전부 등록이 됩니다. 물론, SpringBootTest 속성에 classes 를 지정할 수 있기 때문에, 등록하고 싶은 Bean 만 선택해서 등록할 수 있습니다. 

그럼에도 불구하고 스프링은 실행이 되기 때문에, 단위테스트에 적합하지 않습니다.

글 초반에도 설명했지만, 단위테스트는 외부 의존성이 전혀 없는 상황에서 실행할 수 있어야 합니다. 만약에, classes 를 전혀 지정하지 않는다면, 스프링 부트 에서 ComponentScan 으로 등록 가능한 모든 Bean 을 등록하기 때문에 테스트 실행 시간이 길어집니다. 


관련해서는 필자가 예전에 작성한 글을 읽어보길 바랍니다. 

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


실패하는 테스트 코드를 항상 먼저 작성해야 하나요?

저는, 테스트 주도 개발을 제대로 따르지 못하고 있습니다. 테스트 코드를 작성하고 기능을 구현한 이후에, 다시 테스트 코드를 수정하게 되는 상황이 발생합니다. 설계가 변하면서 테스트 코드도 같이 변하게 되었습니다. 그래서, 항상 테스트 코드를 먼저 작성해야 한다고 개념을 항상 따르지는 않습니다.  


어떤 방식을 선택하든지, 

테스트 코드와 설계는 같이 발전해가는 방향으로 애플리케이션을 만들어갑니다. 


샘플코드

https://github.com/sieunkr/unit-testing/tree/master/test05



글 마무리

테스트 코드를 작성하는 일은 쉽지 않습니다. 최대한 쉽게 글을 작성할려고 노력했지만 글이 너무 길어져서 이 지루한 글을 정독해서 읽어주실 분이 계실지 모르겠습니다. 비록 글은 이상해졌지만, 저는 어제오늘 글을 쓰면서 저는, 테스트 코드 작성에 아직 많이 부족하다는 사실을 깨달았습니다. 이 글을 계기로, 테스트 코드 작성에 좀 더 많은 공부를 해야겠다는 생각이 들었습니다. 


허접한 이 글이 주니어 개발자들에게 조금이라도 도움이 되었기를 바랍니다.

에디의 기술블로그 소속 직업개발자
구독자 666

매거진 선택

키워드 선택 0 / 3 0

댓글여부

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

브런치 로그인