서비스 계층 어떻게 테스트 해야 하는가?

영슈·2024년 4월 30일
1
post-thumbnail

이번 우테코 미션을 하며
MVC 패턴을 통해 Controller-Service-Repository 를 분리했다.

그리고, 우테코의 레벨1 때 요구사항을 생각하며
구현에 대해서는 테스트를 진행하려고 했다.

이때 문제점이 발생했다.

public class ReservationService {  
    private final ReservationRepository reservationRepository;  
    private final ReservationTimeRepository reservationTimeRepository;
    
	public ReservationCreateResponse createReservation(final ReservationCreateRequestInService request) {  
	    final long reservationTimeId = request.timeId();  
	    final ReservationTime reservationTime = 
		    reservationTimeRepository.findById(reservationTimeId)  
	            .orElseThrow(() -> new NotExistReservationTimeException(reservationTimeId));  
	            
	    final Reservation reservation = Reservation.builder()  
	                                               .name(request.name())  
	                                               .date(request.date())  
	                                               .time(reservationTime)  
	                                               .build();  
	  
	    final long reservationId = reservationRepository.create(reservation, reservationTimeId);  
	    return ReservationCreateResponse.from(reservationId, reservation);  
	}
}

해당 코드와 같은 로직이 있을때 repository 의존성을 어떻게 처리하여 테스트를 할지에 대해서다.

사전지식

  • Sociable Test(협동 테스트) : 테스트 대상 유닛이 다른 유닛과 협동하는 관계면 다른 유닛도 함께 테스트한다
final long id = reservationTimeRepository.create(ReservationTime.from("10:00"));  
  
final var result = sut.createReservation(  
        new ReservationCreateRequestInService("조이썬", "2023-10-03", id));  
  
Assertions.assertThat(result)  
          .isInstanceOf(ReservationCreateResponse.class);
  • Solitary Test(단독 테스트) : 테스트 대상 유닛만 테스트한다
//Given  
when(reservationTimeRepository.findById(1))  
        .thenReturn(Optional.of(ReservationTime.from(1L, "10:00")));  
when(reservationRepository.create(  
        Reservation.from(  
                null,  
                "조이썬",  
                "2021-10-03",  
                ReservationTime.from(1L, "10:00")), 1L))  
        .thenReturn(1L);  
  
//When  
final var actual =  
        sut.createReservation(  
                new ReservationCreateRequestInService("조이썬", "2021-10-03", 1l)  
        );  
  
//Then  
assertThat(actual).isInstanceOf(ReservationCreateResponse.class);

협력테스트의 문제점?

처음 단순한 접근으로는 실제 객체를 사용하는 협력 테스트가 좋다고 생각했다. ( 하단 이유 참조 )
1. DB와 실제 연결을 하여 검증을 한다
2. 모킹을 통해 지정시, 테스트 코드가 구현에 대해 어느정도 알아야만 한다
3. when 절로 인해 가독성이 떨어진다

하지만, 우아한 기술 블로그 서버사이드 테스트 파랑새를 찾아서 에서는
유닛 테스트 작성 시 테스트 대상 유닛과 다른 유닛 협동, 위임 관계 존재하는 테스트는 단독 테스트 & 테스트 대역을 적극 사용한다 라는 원칙에 합의했다고 한다

@SpringBootTest 로 인한 테스트 피드백 속도 저하

유닛테스트를 협동 테스트로 구현하려면, 서비스가 의존하는 다른 클래스들 전부를 런타임떄 필요로 한다
-> Spring 이 자동 주입 해주지 않아?
-> SpringBootTest 를 사용하면 되는디?

SpringBootTest를 통해 IoC 컨에니러를 사용할 경우 테스트 피드백이 치명적으로 느려진다.

beforeEach - Mock
@BeforeEach  
void setUp() {  
    this.reservationRepository = mock(ReservationRepository.class);  
    this.reservationTimeRepository = mock(ReservationTimeRepository.class);  
    this.sut = new ReservationTimeService(reservationTimeRepository, reservationRepository);  
}

300

beforeEach 를 사용할때는 매우 느림!

생성자 - Mock
public ReservationTimeServiceMockTest() {  
    this.reservationRepository = mock(ReservationRepository.class);  
    this.reservationTimeRepository = mock(ReservationTimeRepository.class);  
    this.sut = new ReservationTimeService(reservationTimeRepository, reservationRepository);  
}

300

생성자를 통해 생성시 어마어마하게 쩐다!

의존성주입 - 실제
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)  
class ReservationTimeServiceTest {  
    ReservationTimeService reservationTimeService;  
    ReservationService reservationService;  
  
    @Autowired  
    public ReservationTimeServiceTest(final ReservationTimeService reservationTimeService, final ReservationService reservationService) {  
        this.reservationTimeService = reservationTimeService;  
        this.reservationService = reservationService;  
    }

300

스프링부트 테스트를 통한 실행으로 - 생성자 주입,setter 주입 전부 동일하다

400

사실, 전체를 실행했을때는 크게 차이가 나지 않았다 ( 내가 잘못 설정한걸 수도 있음, A_StartTest 는 처음 세팅 처리 시간 빼기 위한 클래스 )

DB 의존성

애플리케이션과 매우 밀접하게 연관되어 있는 DB이지만,
결국 애플리케이션과는 다른 생명주기 및 작동을 하는 외부 의존성이다.

Service 가 실제 Repository 객체를 통해 하는 협력 테스트는 필연적으로 DB 라는 자신이 통제할 수 없는 외부 의존성에 의존이 된다.
-> h2 DB로 테스트 하면 되는거 아니야?

단위 테스트:생산성과 품질을 위한 단위 테스트 원칙과 패턴 해당 책에선 h2 테스트를 권하지 않고 있다.

인메모리 DB를 통해 서로 분리하는 방법을 피할 수 있으나
( 테스트 데이터 제거할 필요 X, 작업 속도 향상, 테스트 실행 마다 인스턴스화 가능 )

일반 DB와 기능적으로 일관성이 없으므로 사용하지 않는 것이 좋다.
( 운영 환경 - 테스트 환경이 일치하지 않게 된다 - 거짓 양성,거짓 음성 발생하기 쉬워진다! )

그러면, 테스트 데이터가 겹치지 않게 엄격하게 관리 & 테스트 코드를 생각해서 작성하면 되는거 아니야?
400

😮‍💨😮‍💨
단순히 생각하면

@Test  
@DisplayName("도메인을 통해 DB에 저장한다.")  
void create_reservationTime_with_domain() {  
    final var reservation =  
            Reservation.from(null, "조이썬", "2024-10-03",  
                    ReservationTime.from("10:00"));  
    reservationRepository.create(reservation, 1);  
  
    final var result = reservationRepository.findAll();  
    assertThat(result).hasSize(1);  
}

테스트 코드를 누가 작성하든, 다른 테스트 코드를 보거나, 컨텍스트를 신경안쓰고
단순히 짤수 있어야 한다는 설명이다. ( 해당 내용&코드는 틀릴수도 있을거 같다. )

결국, 테스트는 각각 독립적으로 외부 의존성(DB...)에 의존 받지 않고 수행이 되어야한다!


이렇게, 협력테스트도 협력테스트 만의 문제점이 존재한다
그러면 단독테스트를 사용해야할까?
그렇지 않다.

단독테스트의 반대파

해당 내용에 대해서 리뷰어님들과 코치들에게 물어보았다.

350

350

그리고, 아래와 같은 의견들을 받았다.

  • 대부분의 버그는 DB에서 나고, 특히 서비스 로직에서 DB 조작할 때 납니다.
    그래서 전 서비스 테스트에서 타 서버 API나 다른 리소스 받아오는 건 mocking하더라도 DB 만큼은 의존성을 넣어서 테스트해야된다는 주의입니다.

  • 오히려 service 테스트를 할때 repository를 모킹함으로써 발생할 수 있는 위험에 대해서 저는 더 고민을 많이 하는 편입니다.
    repository가 제공하는 동작을 모킹했을때 실제로는 반환할 수 없는 값을 반환하도록 설정했을 경우
    service의 테스트가 안정적이지 않게 될 위험이 있습니다. 이는 곧 거짓 음성으로 이어지기때문입니다.

이렇게 단독 테스트에 대한 부정적인 견해들 역시도 존재했다.

그대들은 어떻게 서비스 계층을 테스트 할 것인가

둘다 별로면 어떻게 해야할까?

정답은 둘다라고 결론지었다.
코치 네오는 테스트의 종류, 구현 방법이 관계 없다고 말했다.

중요한건, 테스트를 하려는 목적과 의도가 뭔지를 명확히 정하는 것이라고 했다.

나는 실제 DB와 연결해서, 서비스가 이런 값을 의도하는걸 꼭 봐야겠어
나는 이 값을 넣으면, 서비스가 이 값을 주면 좋겠어

두 사람의 테스트 코드가 같을까?

위에서 말했던 협력테스트의 문제점 부분에 있는 서버사이드 테스트 파랑새를 찾아서 내용도 보면

400

이렇게, 선물하기 팀 역시도
하나의 테스트 스타일만 고집하지 않는다! ( 일부러, 여기에서 설명하려고 빌드업 했다 👍 )

자신만의 테스트 철학을 만들어 나가는게 중요한 거 같다!

그렇기에, 나는 다음 미션때는 모킹을 활용한 단독 테스트를 해볼 예정이다!

  • 서비스는 서비스 로직 그 자체를 온전히 테스트 하기 위해 repository 의 값을 의도적 제어
  • 통합 테스트에서 실제 DB를 사용해 모든 로직이 DB와 함께 의도적으로 작동함을 검증

마지막으로 제일 중요한 이유로 해보지 않았으니까!
( 경험하면서 장단점을 느껴볼 예정이다 )

400

참고

해당 미션 저장소

서버사이드 테스트 파랑새를 찾아서

단위 테스트 : 생산성과 품질을 위한 단위 테스트 원칙과 패턴

0개의 댓글