TDD를 위한 데이터 - 대역

크리링·2024년 2월 5일
0

TDD 정리

목록 보기
3/5
post-thumbnail

문제

TDD를 실무에서 사용하다보니 문제를 찾았다. 실시간 데이터를 테스트할 때 자동화하기 어렵다는 문제를 발견했다. 처음에는 해결할 방법을 찾지 못해 실시간 데이터를 @BeforeEach를 통해 모든 단위 테스트 별로 save하는 방식을 사용했는데, 이 방법에도 문제가 있었다.

매 단위 테스트는 @Transactional 어노테이션이 걸려 데이터는 롤백이 되지만 만약 AUTO_INCREMENT 방식을 사용한다면 인덱스 값에 공백이 생긴다는 문제시간이 많이 걸린다는 문제다.

이 방법은 최범균님의 테스트 주도 개발 시작하기에서 대역이라는 개념을 공부해서 해결해보려 한다.






대역

테스트 대상에서 의존하는 요인 때문에 테스트가 어려울 때 대역을 써서 테스트를 진행한다.
영어로는 Test Double

테스트 대상이 이러한 외부 요인에 의존하면 테스트를 작성하고 실행하기가 어려워진다.

외부 요인이 테스트에 관여하는 주요한 예로는

  • 테스트 대상에서 파일 시스템을 사용
  • 테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가
  • 테스트 대상에서 외부의 HTTP 서버와 통신

이 있다. (내 경우는 두번째와 세번째에 해당했다.)



종류

대역에는 종류가 있다.

  • 스텁(Stub)
    • 구현을 단순한 것으로 대체한다. 테스트에 맞게 단순히 원하는 동작을 수행한다.
  • 가짜(Fake)
    • 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. (DB 대신에 메모리를 이용해서 구현한 가짜 대역)
  • 스파이(Spy)
    • 호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. (스텁이기도 하다.)
  • 모의 (Mock)
    • 기대한 대로 상호작용하는지 행위를 검증한다. 기대한 대로 동작하지 않으면 Exception을 발생할 수 있다. (모의 객체는 스텁이자 스파이도 된다.)

주로 사용하는 Stub과 Mock을 코드로 확인해보자

스텁

이전에 일단 사용했던 Dollar 테스트 코드에 id 필드를 추가하고, 자주 사용하는 JpaRepository로 먼저 시작해보았다. 기능 구현에 필요한 id와 생성자 부분을 추가했다.


테스트 폴더에 DollarStubRepository를 생성한다.

implements 선언을 하고 Repository 어노테이션을 추가한다.

오버라이드를 일단 선언하여 빨간줄을 지운다. (java.lang.Object의 내용만 제외하고 모두 오버라이드)

대부분의 기능은 null을 리턴하거나 동작하지 않는 빈 메서드지만 주로 사용하는 내용들만 기능을 추가해본다.

가장 먼저 Map과 id를 필드로 선언

주요 메서드 동작 구현
saveAll()findAll() savefindById, count

필요한 메서드를 구현하고 이전에 만들어놓은 DollarService에 의존관계를 주입한다.

생성자 확인을 위해 StubRepository에 몇 데이터를 추가한다.

단위 테스트를 작성하고 실행

Stub 기능을 해봤지만 문제가 있다. 일부 기능을 위해 다양한 설정을 오버라이딩하고 복잡해도 Configuration 주입 설정을 해줘야 한다는 문제가 있다. 찾아보니 Stub을 사용할 때 JpaRepository 보다는 필요한 기능만 사용하는 그냥 Repository 어노테이션을 선호하기도 한다고 한다.



Mock

다음과 같이 MockTest 코드를 만들었다.

  • @ExtendWith(MockitoExtension.class) : Mock을 사용하기위해 선언해 주어야 한다. (메인으로 실행될 Class를 지정할 수 있음)
  • @Mock : Mock 사용 지정
  • @InjectMocks : 지정된 Mock을 주입 받는 객체

(TDD 원칙 중 given / when / then 으로 나누는 부분에서 Mock 기능은 given에 가깝다고 생각해서 BDDMockitogiven을 사용하지만 when 을 사용해도 상관 없다고 함)

실행해보면

바로 실패

근데 이상한 점을 발견했다.

@ExtendWith(MockitoExtension.class) 어노테이션을 제거하면 잘 동작한다.
반대로 @SpringBootTest 어노테이션을 제거하고 @ExtendWith 어노테이션을 살려도 동작한다.

둘 사이에 어떤 문제가 있는걸까?

@SpringBootTest 어노테이션의 세부 내용을 보면 다음과 같이 @ExtendWith어노테이션이 있는데 두 같은 어노테이션이 충돌한 것으로 보인다.

@SpringBootTest

  • 통합 테스트용으로 사용
  • @SpringBootApplication을 찾아가 하위의 모든 Bean 스캔 및 로드
  • Test 용 Application Context를 만들어 Bean을 추가하고 MockBean을 찾아 교체

하는 역할을 하는 어노테이션이다.

오류의 원인도 찾았고, 테스트도 초록불이니 코드를 조금 정리해본다.






의문 정리

Stub과 Mock의 결정적 차이?

라는 의문에 내가 찾은 답에 간단히 정리해보면

  • Stub은 준비한 가짜 데이터에 초점
  • Mock은 행위를 테스트하는 것에 초점



Mock과 Spy의 차이

Spy는 일부 메소드에 대하여 Mocking이 가능한 점이 Mock과 다르다.

일부 Mock 데이터를 InjectMocks 주입받은 객체에서 사용할 때 Mock 데이터여서 오류가 날 경우 이 주입받은 객체에 @Spy 어노테이션을 추가해 실제 구현과 동일하게 동작하도록 만들 수 있다고 한다. (참고)



Classist와 Mockist

TDD 방식 중에 ClassistMockist가 있다고한다.

  • Classist
    • 대부분 실제 객체 사용을 선언하고, 실제 객체 사용이 어렵다면 테스트 더블을 사용한다.
    • 실제 객체만 사용하므로 상태 검증을 하게된다.
  • Mockist
    • 가짜 객체를 사용하여 의존성을 제거한다.
    • Service 테스트시 repository를 가짜 객체로 사용하는 것과 같다.
    • 가짜 객체를 사용하기 위해 내부 구현을 커스텀한 응답으로 응답해야 한다.
      특정 행위가 실행되었는지 행위검증한다.

지금 나는 TDD 공부 이전에 간단히 하는 테스트는 Classist에 가까웠지만 지금은 데이터를 만들기가 너무 어렵다는 이유로 Mockist에 가깝운 상태로 진단된다. 잘 섞어서 어떤게 올바른 검증이고, 변동이 적은 자동화된 테스트임을 고민하면서 써야겠다.






출처 및 참고

부족한 부분은 댓글 달아주시면 공부하고 적용해보겠습니다. 읽어주셔서 감사합니다.

0개의 댓글