스프링 공부/인프런 김영한 스프링 입문 노트정리

3-5. 회원 서비스 테스트

모항 2022. 7. 28. 19:15

저번 시간에 만든 서비스를 테스트해보는 회차이다.

 

 

 

테스트 코드의 틀을 짜주는 단축키

이번 회차에도 역시 IntelliJ의 편리한 기능이 하나 더 등장했다.

클래스의 구조 및 위치를 바탕으로, 이 클래스를 테스트하기 위한 테스트 코드의 기초적인 틀을 생성해주는 기능이다.

 

우리가 테스트할 MemberService 클래스에 이름에 커서를 위치시키고 단축키를 누른다. Windows 기준으로 ctrl+shift+T이다.

그럼 이런 것이 뜨는데, Create New Test를 선택한다.

 

그러면 새로 만들어질 테스트에 대한 설정 창이 뜬다.

이름은 기본적으로 테스트 대상 클래스의 이름 뒤에 Test를 붙인 것으로 생성되어있다.

 

Testing library는 JUnit5로 해주고

클래스 내의 모든 메소드를 테스트할 것이므로 하단 목록에서 모든 메소드를 체크해준다.

OK를 누른다.

 

그럼 이렇게 테스트 코드의 틀을 짜준다.

test 하위에 새롭게 만들어진 이 테스트 코드의 위치를 보면, main의 테스트 대상 코드와 대칭인 자리에 잘 자리잡았다는 것을 알 수 있다.

 

 

 

 

 

 

 

 

MemberService에서 MemberRepository를 새로 만들지 않도록 하기 (DI)

본격적으로 테스트 코드를 작성하기 전에

우리가 고쳐야 하는 것이 하나 있다.

 

MemberService를 보면 다음과 같이,

MemberService 객체가 하나 생성될 때마다

본인의 내부에서 사용할 MemberRepository (MemoryMemberRepository) 객체를 자체적으로 새로 만들도록 되어있다.

이렇게 해놓으면 문제를 일으킬 가능성이 크다고 한다!

 

일단 우리가 테스트 코드를 작성하는 데서부터 문제가 생길 수 있다.

예를 들면 다음과 같다.

여기 보면 MemberService 내에서 새롭게 선언되는 MemberRepository 요놈은 private이다.

(여러 가지 이유에서 private로 해놓는 것이 좋기 때문에 private로 해놓은 것임)

 

 

근데 저번에 다른 테스트 만들 때 배웠듯이,

우리는 @AfterEach를 이용해서,

한 테스트가 끝날 때마다 메모리를 깨끗이 비울 수가 있다.

 

근데 메모리를 비울 때 쓰는 clearStore() 메소드가 어디에 들어있는가?

Service에 들어있는가? 아니다! Repository에 들어있다.

 

따라서 이 비워주는 메소드를 가져다 쓰려면,

Service 안에 들어있는 Repository에

외부로부터 접근을 해서

메소드를 호출해야 한다!

 

 

근데 아까 말한 것처럼 Service 내의 Repository 객체는 private이다.

그래서 이렇게, 외부에서 접근할 수가 없다. 빨간 글씨가 된다.

 

우리는 MemberService 객체를 하나 만들어놓고서는 그 안에 있는 Repository 객체를 꺼내 쓰질 못해서 메모리 비우기를 못하는 멍청한 상황에 처한 것이다.

 

예를 들면 다음과 같다.

 

메모리 비우기를 갖다쓰려면

MemberService 객체를 멀쩡히 만들어놨더라도

MemberRepository 객체를 새롭게 하나 만들어야 한다.

그렇게 하면 MemberService 하나, MemberRepository 하나 이렇게 두 객체가 따로 존재하게 되고

두 객체가 사용하는 DB 또한 따로 분리된다.

 

MemberService memser와 MemberRepository memrepo를 하나씩 만들었다고 하자.

memser를 이용하여 join()을 실컷 테스트한 뒤에 이 memser의 DB를 clear하고 싶은데

그게 안 돼서 memrepo의 clearStore()를 꺼내 실행하면

memser 내부의 MemberRepository와 memrepo는 서로 동일한 객체가 아니기 때문에

memser 객체 내부의 DB가 아닌, 아예 다른 DB(memrepo의 DB)가 clear되어버린다는 것이다.

 

 

더보기

근데 사실 지금은 저렇게 memser와 memrepo를 따로 만들어 사용해도 DB가 쪼개지지 않는다!

Repository에서 DB로 사용하는 이 store이라는 놈의 성질이 static이기 때문에

 

 

이렇게 Service 객체 하나, Repository 객체 하나 따로 만들어서 사용해도

 

지금 당장은!!!

 

문제가 없다.

 

Service 객체의 DB와 Repository 객체의 DB가 서로 따로따로 놀지 않고, 하나의 DB처럼 잘 작동해준다. static 덕분에!

 

대체 static이 어떤 역할을 하기에 이런 결과가 나오는지는 객체지향 프로그래밍에 대한 공부를 더 해봐야 알 수 있을 것 같다. 나중에 더 찾아보자.

강사님의 말씀에 따르면 static인 덕에 store이 클래스 단위에 붙어서 작동하기 때문이라고 한다.

 

하지만!

store에서 static을 지워주기만 해도 바로 문제가 발생할 것이다.

 

그러므로 장기적인 관점에서 볼 때는 Service 코드를 바꿔주는 게 맞다.

 


 

 

Service 코드를 다음과 같이 바꾸면 된다.

 

원래 이랬던 코드에서

 

new로 새롭게 만들어주던 부분을 지워서 선언만 남기고,

IntelliJ의 편리한 기능을 사용해 생성자를 만들어준다.

이렇게 해주면 된다.

 

Service 자체적으로는

MemberRepository를 new 하게 만들어주지는 않고! 선언만! 해둔 채로

외부에서 MemberRepository 객체를 받아와서

"우리의 MemberRepository는 외부에서 받아온 이놈이란다"

라고 지정만 해주는 것이다.

(코테 할 때 아무 생각 없이 맨날 쓰던 방식인데 이게 이렇게 깊은 뜻이 있는 코딩법이었다니...)

 

이렇게 하면

어떤 Service 객체와 어떤 Repository 객체를 짝지어줄지를

우리 맘대로 정할 수 있게 된다!

 

 

 

이런 방식의 코딩을 DI (Dependency Injection) 이라고 한다.

이것에 대한 더 자세한 설명은 다음 강의에서 해주신다고 하셨다.

 

 

 

여기까지 했으면 이제 테스트 코드들을 만들어줄 것이다.

@BeforeEach,

회원 가입이 정상적으로 잘 되는지 보는 테스트,

회원 가입 시 이름이 중복될 경우 예외가 잘 throw되는지 보는 테스트

이렇게 만들어줄 것이다.

 

 

 

 

 

객체 선언과 @BeforeEach

테스트받을 객체는

수정해준 생성자 형식에 맞게

위와 같이 각 테스트를 시작하기 전에 매번 새로 만들어지도록 하겠다.

 

 

 

 

Given, When, Then

강사님께서 테스트 코드를 쓸 때의 팁을 하나 알려주셨다.

코드를 given, when, then의 구조로 나누어 써보면 좋다는 것이다.

 

 

given 이하에는 테스트를 받을 대상을 마련하고

when 이하에서는 대상에 대하여 취해지는 어떤 행위를 하고

then 이하에는 그래서 어떤 결과가 나와야 하는지에 대한 코드를 적는다.

 

이 구조가 모든 경우에 100% 딱딱 들어맞는 것은 아니기 때문에 적당히 수정해가면서 쓰면 된다고 한다.

그러나 주석으로 일단 //given //when //then부터 적어놓고 시작하기만 해도, 테스트의 방향을 잡는 데 도움이 된다고 한다.

 

 

 

 

 

회원가입 테스트

테스트 만들기를 시작하기 전에

이놈들을 import해주면 편하다는 걸 잊지 말자.

 

 

 

회원가입 테스트는

정상적으로 회원가입이 이루어졌을 경우 가입된 회원의 정보가 올바른지를 테스트하는 것 하나,

이름이 중복되는 회원의 가입이 시도되었을 경우 예외가 잘 throw되는지 테스트하는 것 하나

총 두 가지를 만들 것이다.

 

먼저 정상적인 회원가입부터 테스트한다.

이렇게만 해줘도 된다.

회원 객체 하나를 회원가입하고,

회원가입 시에 리턴된 결과값(ID)가

회원가입된 회원의 ID와 동일한지 보는 것이다.

테스트도 잘 통과된다.

 

강사님은 findOne까지 넣어서 아래와 같이 짜셨다.

이것도 잘 통과된다.

 

이름 말고 Member 객체끼리 비교해도 잘 통과된다.

 

 

 

 

다음으로, 이름이 중복되는 회원을 가입시키려 시도했을 때 의도한 예외가 잘 throw되는지 확인하는 테스트를 만들어보자.

 

강사님께서는 두 가지 방법을 알려주셨다.

첫 번째는 try-catch문을 만들고 그 안에서 assert하는 것

두 번째는 try-catch문을 쓰지 않고 assert하는 것

 

 

먼저 try-catch문을 사용하는 방법이다.

위와 같이 쓰면 된다.

우리의 Service가 정상적으로 작동한다면 mem2를 join하려 시도할 때 반드시 예외가 발생한다.

 

그러므로,

try문 내에서 memser.join(mem2); 를 시도했는데도 예외가 생기지 않는다면 뭔가 잘못된 것이다!

그걸 땐 바로 다음 문장인 fail()이 실행되면서 테스트가 실패하도록 하였다.

반드시 예외가 생겨야만 테스트 성공이기 때문이다.

 

위와 같이 mem2의 이름이 중복되지 않도록 바꾸어서 예외가 throw되지 않게 만들어보면 fail()문이 잘 실행되는 것을 볼 수 있다. 콘솔을 보면 fail() 안에 넣어준 메시지가 잘 뜬다.

 

 

만약 우리가 의도한 예외가 발생하였다면,

Service를 만들 때 정한 대로 그 예외의 종류는 IllegalStateException일 것이고

메시지는 "이미 존재하는 이름입니다."가 될 것이다.

 

그러므로 catch문에서는 IllegalStateException을 잡아주고 예외 메시지의 내용이 "이미 존재하는 이름입니다."가 맞는지를 assert하도록 하였다.

위와 같이 assertThat 문장에서 메시지의 내용을 이상하게 바꿔보면 테스트가 실패하는 것을 볼 수 있다.

 

 

 

 

이렇게 모든 경우의 수를 포괄하는 테스트가 완성되었다.

  1. 만약 예외가 정상적으로 발생했고 그게 우리가 의도한 그 예외가 맞다면! catch문이 정상적으로 작동해서 테스트가 통과된다.
  2. 만약 예외가 발생하기는 했는데 그게 우리가 의도한 예외가 아니라 다른 예외라면 catch되지가 않아서 테스트가 실패한다.
  3. 만약 예외가 아예 발생하지 않았다면 fail()이 실행되어서 테스트가 실패한다.

 

 

 

 

 

다음은 try-catch문을 사용하지 않는 방법이다.

assertThrows()를 사용할 것이다.

필요한 인자는 이렇다.

 

첫째 인자로 IllegalStateException.class를, 둘째 인자로 람다식을 넣어주면 된다.

실행해보면 잘 pass된다.

 

 

 

 

이렇게 람다식이 아닌 일반 문장을 쓰거나

 

첫째 인자에 Exception 타입만 달랑 쓰면 안 된다.

 

 

 

여기다가 예외 메시지 체크까지 해보고 싶으면

assertThrows()에 커서를 옮긴 뒤 기가 막힌 단축키인 ctrl+alt+V를 눌러 리턴값을 받을 변수를 마련해주고

 

그 변수에서 getMessage()로 받아온 메시지 내용을 이렇게 체크해주면 된다. 돌려보면 잘 pass된다.

 

 

 

 

 

 

최종 테스트

얘네는 지워도 될 것 같다.

findMembers()에는 어차피 이전에 테스트를 끝낸 Repository의 findAll() 메소드 한 문장밖에 안 들어있고 findOne()은 join()테스트할 때 테스트가 되었기 때문이다.

 

 

지워준 다음 클래스 전체를 한 번에 테스트해주면,

콘솔 좌측 리스트에 전부 다 초록불이 들어오는 것이 보인다. 완성이다!