이번 우테코 미션을 하며
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 의존성을 어떻게 처리하여 테스트를 할지에 대해서다.
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);
//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 절로 인해 가독성이 떨어진다
하지만, 우아한 기술 블로그 서버사이드 테스트 파랑새를 찾아서 에서는
유닛 테스트 작성 시 테스트 대상 유닛과 다른 유닛 협동, 위임 관계 존재하는 테스트는 단독 테스트 & 테스트 대역을 적극 사용한다
라는 원칙에 합의했다고 한다
유닛테스트를 협동 테스트로 구현하려면, 서비스가 의존하는 다른 클래스들 전부를 런타임떄 필요로 한다
-> Spring 이 자동 주입 해주지 않아?
-> SpringBootTest 를 사용하면 되는디?
SpringBootTest를 통해 IoC 컨에니러를 사용할 경우 테스트 피드백이 치명적으로 느려진다.
@BeforeEach
void setUp() {
this.reservationRepository = mock(ReservationRepository.class);
this.reservationTimeRepository = mock(ReservationTimeRepository.class);
this.sut = new ReservationTimeService(reservationTimeRepository, reservationRepository);
}
beforeEach 를 사용할때는 매우 느림!
public ReservationTimeServiceMockTest() {
this.reservationRepository = mock(ReservationRepository.class);
this.reservationTimeRepository = mock(ReservationTimeRepository.class);
this.sut = new ReservationTimeService(reservationTimeRepository, reservationRepository);
}
생성자를 통해 생성시 어마어마하게 쩐다!
@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;
}
스프링부트 테스트를 통한 실행으로 - 생성자 주입,setter 주입 전부 동일하다
사실, 전체를 실행했을때는 크게 차이가 나지 않았다 ( 내가 잘못 설정한걸 수도 있음, A_StartTest 는 처음 세팅 처리 시간 빼기 위한 클래스 )
애플리케이션과 매우 밀접하게 연관되어 있는 DB이지만,
결국 애플리케이션과는 다른 생명주기 및 작동을 하는 외부 의존성
이다.
Service 가 실제 Repository 객체를 통해 하는 협력 테스트는 필연적으로 DB 라는 자신이 통제할 수 없는 외부 의존성에 의존이 된다.
-> h2 DB로 테스트 하면 되는거 아니야?
단위 테스트:생산성과 품질을 위한 단위 테스트 원칙과 패턴 해당 책에선 h2 테스트를 권하지 않고 있다.
인메모리 DB를 통해 서로 분리하는 방법을 피할 수 있으나
( 테스트 데이터 제거할 필요 X, 작업 속도 향상, 테스트 실행 마다 인스턴스화 가능 )일반 DB와 기능적으로 일관성이 없으므로 사용하지 않는 것이 좋다.
( 운영 환경 - 테스트 환경이 일치하지 않게 된다 - 거짓 양성,거짓 음성 발생하기 쉬워진다! )
그러면, 테스트 데이터가 겹치지 않게 엄격하게 관리 & 테스트 코드를 생각해서 작성하면 되는거 아니야?
😮💨😮💨
단순히 생각하면
@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...)에 의존 받지 않고 수행이 되어야한다!
이렇게, 협력테스트도 협력테스트 만의 문제점이 존재한다
그러면 단독테스트를 사용해야할까?
그렇지 않다.
해당 내용에 대해서 리뷰어님들과 코치들에게 물어보았다.
그리고, 아래와 같은 의견들을 받았다.
대부분의 버그는 DB에서 나고, 특히 서비스 로직에서 DB 조작할 때 납니다.
그래서 전 서비스 테스트에서 타 서버 API나 다른 리소스 받아오는 건 mocking하더라도 DB 만큼은 의존성을 넣어서 테스트해야된다는 주의입니다.
오히려 service 테스트를 할때 repository를 모킹함으로써 발생할 수 있는 위험에 대해서 저는 더 고민을 많이 하는 편입니다.
repository가 제공하는 동작을 모킹했을때 실제로는 반환할 수 없는 값을 반환하도록 설정했을 경우
service의 테스트가 안정적이지 않게 될 위험이 있습니다. 이는 곧 거짓 음성으로 이어지기때문입니다.
이렇게 단독 테스트에 대한 부정적인 견해들 역시도 존재했다.
둘다 별로면 어떻게 해야할까?
정답은 둘다라고 결론지었다.
코치 네오는 테스트의 종류, 구현 방법이 관계 없다고 말했다.
중요한건, 테스트를 하려는 목적과 의도가 뭔지를 명확히 정하는 것이라고 했다.
나는 실제 DB와 연결해서, 서비스가 이런 값을 의도하는걸 꼭 봐야겠어
나는 이 값을 넣으면, 서비스가 이 값을 주면 좋겠어
두 사람의 테스트 코드가 같을까?
위에서 말했던 협력테스트의 문제점
부분에 있는 서버사이드 테스트 파랑새를 찾아서 내용도 보면
이렇게, 선물하기 팀 역시도
하나의 테스트 스타일만 고집하지 않는다! ( 일부러, 여기에서 설명하려고 빌드업 했다 👍 )
자신만의 테스트 철학을 만들어 나가는게 중요한 거 같다!
그렇기에, 나는 다음 미션때는 모킹을 활용한 단독 테스트를 해볼 예정이다!
마지막으로 제일 중요한 이유로 해보지 않았으니까!
( 경험하면서 장단점을 느껴볼 예정이다 )