DTO(repository) 레이어에서는 spring data jpa를 쓰는 경우 @DataJpaTest와 TestEntityManager를 사용하면 원하는 대로 테스트를 할 수 있다. testEntityManager로 jpa의 모든 동작(persist, delte 등)을 사용할 수 있다. 테스트를 위해 넣은 데이터는 나중에 롤백되기 때문에 데이터베이스에 영구적인 영향을 미치지 않는다. spring data jpa 자체가 이미 검증된 모듈인데 테스트할 필요가 뭐가 있냐고 생각할 수 있다. 그 말에 동의한다. 그래서 @Query를 사용해서 내가 지정한 쿼리를 날리는 메소드만 테스트 중이다.
service layer를 테스트하려 했는데 도대체 뭘 테스트 해야겠는 지 모르겠었다.
@ExtendWith(MockitoExtension.class)
일단 spring context를 load하지 말고 가볍게 테스트 해야한다. service layer에 대한 단위 테스트를 만들 거니까 말이다. 그래서 mock을 사용한다. service layer에서 의존관계 주입이 이루어지는 빈들은 @Mock 을 적용한다. 그리고 테스트하고자 하는 service class에는 @InjectMocks를 사용해서 @Mock이 붙은 빈들을 주입 받는다.
그런데 이렇게 Double Mock을 하게 되니 repository에서 doReturn을 사용해서 내가 임의의 값을 리턴하게 한다. 나는 id가 1인 유저를 service layer에서 저장 요청할 경우 DB에 잘 저장되는 지를 확인하고 싶었다. mock으로 주입된 userRepository애서 실제 메소드를 호출할 방법을 찾을 수 없었다. doCallRealMethod()라는 게 있어서 시도해봤지만 추상 메소드에는 적용할 수 없었다. 결국 repository에서는 내가 임의로 리턴 값을 지정해줘야 하는데 service layer에서의 리턴 값이 내가 지정한 리턴 값이 맞는 지 확인하는 건 정말 의미 없는 행동이라는 생각을 했다.
그러면 service layer에서는 뭘 테스트해야 할까?
일단 하위 layer에서의 동작은 정상 작동한다는 가정을 깔고 간다. 이를 위해서는 하위 layer에서의 테스트가 충분히 작성되어야 할 것이다.
인터넷 상(블로그나 stack overflow)의 테스트 코드들을 살펴본 결과 service layer에서 이러한 것들을 테스트하고 있었다. 몇 가지는 mockito에서 제공되는 메소드를 바탕으로 생각한 것이다.
- 매개변수가 제대로 바인딩되는 지
- ArgumentMacher 사용
- 비즈니스 로직 상에서 매개변수를 특정 객체에 잘못 바인딩하지 않나 테스트하는 것이다. 예를 들어 생성자의 매개변수에 의도와 다른 값을 주입하는 실수를 방지해준다.
- 내가 주입해서 사용하는 컴포넌트의 어떤 메소드가 호출되는 지 확인( + 몇 번 호출되는지 확인 가능)
- verify() 사용
- userService에서 user를 새로 생성할 때 userRepository에서 save()를 호출하는 지 확인할 수 있겠다.
- 당연한 거를 테스트하는 거 아닌가 싶었다. 하지만 TDD 개발을 할 때 테스트 코드를 미리 작성해서 스펙을 정의한 뒤 userService 코드를 작성하는 거라면 userRepository의 save()를 호출하지 않은 경우 확인시켜주는 용도로 볼 수 있지 않을까?
- 특정 상황에서 예외를 던지는 지 확인
- assertThrow() 사용
- 접근 권한이 없을 때 예외를 던지게 비즈니스 로직을 짰다면 접근 권한이 없는 상황을 만든 뒤 비즈니스 로직에서 예외를 실제로 던지는 지 확인이 가능하다.
뜬금없지만 Builder의 문제
특정 상황을 만들어줄 때 예기치 못한 문제가 있었다. controller layer를 간소화하기 위해 엔티티 객체가 아닌 엔티티 id 값을 서비스 레이어의 메소드에 넘겨줬다. 그래서 서비스 레이어에서 findById가 필요했는데 이 리턴 값을 mocking 할 필요가 있었다. 테스트를 원활하게 하려면 내가 doReturn 해주는 엔티티 객체에 id 값이 있어야 했는데 id 값을 매개변수로 갖는 생성자는 만들지 않았었다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_id")
private Long id;
id를 DBMS에 달라고 요청하는 GenerationType.IDENTITY 전략을 사용하기 때문에 id 값을 내가 직접 넣어줄 필요가 없었다. 그런데 테스트를 위해서는 내가 지정해준 id 값을 갖는 엔티티 객체를 만들 필요가 있었다. 생성자를 새로 만들까 했지만 생성자는 매개변수 개수가 많아지면 어디에 뭘 넣어야 할 지 헷갈리기도 하고 엔티티 클래스의 코드가 길어지는 결과를 낳는다. 그래서 lombok의 @Builder를 엔티티 클래스 위에 적어줬다.
여기서 주의할 점이 있다. Builder는 아직 완전하게 개발됐다고 생각하지 않는다.
차근차근 문제점을 보자.
private List<Board> boards = new ArrayList<>();
일단 다른 엔티티와의 연관관계를 맺기 위해 List 타입의 필드를 뒀다면 빈 리스트로 기본값을 줬을텐데 Builder로 생성하는 경우를 위해 @Builder.Default를 해당 필드 위에 적어줘야 한다. 그래야 Builder로 build 시 저 기본값으로 세팅이 된다. 이러면 Builder를 통해서 엔티티 객체를 생성 시에는 정상적인 반면, new 키워드와 생성자를 통해 엔티티 객체를 생성하면 문제가 있다. 저 기본값이 적용이 되지를 않는다. 그래서 boards.XX를 사용 시 null pointer exception이 발생해버린다.
뭔가 다른 방법이 있지 않을까 싶어 찾아봤지만 lombok 개발팀에서 오랫동안 이 문제를 해결하지 않은 것으로 보인다.
https://github.com/projectlombok/lombok/issues/1347
그러니 Builder 사용 시 깔끔해지긴 하지만 생성자와 혼용 시 에러 발생(NPE)이 가능하다는 건 유념해야 한다.
다시 테스트로 돌아와서 정리
현재 service layer 코드를 다 구현해놓고 테스트 코드를 짜고 있는 입장에서 보면 3번을 제외한 1번, 2번은 너무 당연한 과정이라 필요가 없어 보인다. 하지만 내가 service layer를 개발하며 디버깅 했던 과정들을 돌이켜봤다. 테스트 코드를 미리 짜놓고 이를 통과시키기 위한 service layer 구현을 했다면 저 당연해보이는 테스트도 디버깅 시에 유용했을 것이다. 알고리즘 문제를 풀기 위해 몇십줄 짜리 코드를 작성할 때도 생각지도 못한 곳에서 논리 오류를 발견하게 된다. 이를 고려하면 테스트 코드 작성은 무쓸모가 아니다. 저 당연해보이는 테스트들도 바보같은 실수를 할 지 모르는 나를 위한 방패라고 생각하면 되지 않을까 싶다.
'프로젝트 > 크루트' 카테고리의 다른 글
ec2를 종료해서 도커와 젠킨스가 죽었을 때 살리는 법 (0) | 2022.05.02 |
---|---|
jpa의 orphanRemoval이 잘 작동하는 지 테스트가 하고 싶을 때 (0) | 2022.04.01 |
Entity에 @Transactional을 쓰고 싶은데 그래도 될까? (0) | 2022.03.28 |
모바일 브라우저에서는 로그인이 안 되는 이유 (0) | 2022.03.25 |
https와 cross domain으로 서버와 통신 시 쿠키 보내는 법 (0) | 2022.03.24 |