Server/JUnit, Spock

ATDD 이해해보기!

JaeHoney 2023. 5. 17. 08:05

최근 TDD에 큰 관심이 생겨서 요즘 핫하다는 ATDD에 대해서와 TDD에 대해 더 깊게 이해하려고 했다.

 

강연을 찾던 중 NEXTSTEP에 ATDD, 클린 코드 with Spring이라는 좋은 강의 코스가 있는데, 가격이 80만원이다... ㄷㄷ

사실 조금 부담스러워서 우아한 ATDD라는 우아한 테크 세미나 발표가 있다.

  • 아래는 해당 발표 내용을 공부하며 정리한 것이다..!

사실 메모에 가깝다.

인수 테스트

인수 테스트는 시나리오(사용자 스토리) 기반으로 기능을 테스트하는 것이다.

이러한 인수 테스트는 아래의 장점이 있다.

  • 배포 없이 빠른 피드백을 받을 수 있다.
  • 도메인과 서비스 파악에 큰 도움이 된다.
  • QA 절차 없이 테스트 코드만으로도 동작을 검증할 수 있다.

TDD에서의 문제

TDD에서는 테스트 코드를 어떻게 시작하고, 어떻게 끝낼 지에 대해 명확하지 않다는 점이 있다.

 

추가로 각 단위들이 통합이 잘 되는 지 알 수 없다.

  • 예를 들어 외부 API를 사용하는 경우, 외부 DB 서버를 사용하는 경우, ...

레거시 코드에서의 테스트 작성은 더 막막하다.

ATDD로 해결할 수 있는 문제

ATDD(인수 테스트 주도 개발)를 하면 위 문제들을 해결할 수 있다.

 

ATDD로 큰 틀(인수 조건)을 먼저 작성한 후 그거에 맞춰서 단위 테스트를 작성할 수 있다.

  • 작업의 시작과 끝이 명확해져서 심리적인 안정감에 도움을 준다.
  • 이해 관계자들이 요구사항을 일관되게 이해할 수 있다.

ATDD 사이클

ATDD의 개발 사이클은 아래와 같다.

ATDD 사이클

ATDD 사이클의 단계를 세부화해보면 아래와 같다.

  • 전체적인 인수 시나리오 및 전반적인 기능을 설계한다.
  • 인수 테스트를 작성한다.
  • 핵심 기능에 대한 세부적인 도메인 설계를 한다.
  • 도메인 객체에 대한 TDD를 수행한다.

인수 테스트 작성

ATDD를 작성할 때는 먼저 인수 테스트가 충족해야 하는 조건을 작성한다.

 

인수 테스트의 조건은 아래의 기준으로 작성한다.

  • 검증하고자 하는 when 구문 먼저 작성
  • 기대 결과를 의미하는 then 구문 작성
  • when과 then에서 필요한 정보를 given에서 마련

예를 들어 아래와 같이 작성할 수 있다.

given
  강사는 강의를 생성했다.
  강사는 강의를 신청 가능 상태로 변경했다.
  강의 모집인원 만큼 신청을 받았다.
when
  회원이 수강 대기 신청을 요청한다.
then
  회원은 강의의 수강 대기자로 등록 되었다.

이제 조건을 바탕으로 테스트를 작성하면 된다. 주의할 점은 사용자 관점에서 실제로 요청을 보내고 요청을 받는다는 점이다.

코드는 위와 같다.

인수 테스트 특징

인수 테스트는 내부 구조나 작동과 연관이 없는 Black box 테스트에 해당한다.

  • 사용자 관점에서 잘 동작하는 지에 집중한다.

UI 레벨에서 인수 테스트를 하면 변동성이 커서 깨지기 쉽고 서버 개발자에게 부담스럽다. 그래서 API 레벨에서 인수 테스트를 작성해나간다.

인수 테스트 단위

특정 도메인에 대해서 모든 테스트를 한 클래스에 모아두면 정말 테스트에서 필요한 given 절이 맞는 지에 대한 의문이 생기게 된다.

 

그래서 같은 given절(+ 테스트 픽스처)를 공유하는 클래스끼리 모아서 인수 테스트 클래스 단위를 만든다.

  • 다른 개발자가 해당 테스트에서 필요한 given 구문을 쉽게 파악할 수 있다.
  • 결과적으로 코드 응집도가 높아진다.

인수 테스트 도구

인수 테스트 시 아래의 테스트 도구를 활용할 수 있다.

 

[Server tool]

  • @SpringBootTest

[Client tool]

  • MockMVC, WebTestClient, RestAssured

WebEnvironment

인수 테스트에서는 실제 사용자 환경과 가장 유사한 @SpringBootTest를 사용한다.

@SpringBootTest의 webEnvironment = RANDOM_PORT 또는 DEFINED_PORT 설정을 사용한다.

  • Tomcat, netty 실제 웹 환경과 유사한 환경을 구축할 수 있다.
  • 별도의 쓰레드에서 스프링 컨테이너가 실행되어서 트랜잭션이 롤백되지 않을 수 있다.
  • default 값은 MOCK으로 실제 ServletContainer를 띄우지 않는다.

더 자세한 내용은 아래 블로그를 참고하자.

참고: https://mangkyu.tistory.com/264

RestAssured

@SpringBootTest에서 webEnvironment = RANDOM_PORT로 사용하면 MockMvc를 사용할 수 없다.

 

대표적인 Client 도구는 아래의 3가지가 있다.

MockMvc는 사용할 수 없고 WebTestClient의 경우에는 WebFlux의 패키지 안에 포함되어있고 Nettty를 기본으로 사용한다.

 

그래서 RestAssured를 사용하면 실제 내장 Tomcat에 요청을 보내고 응답을 받을 수 있다.

위와 같이 스프링 부트 포트를 RestAssured.port에 주입해줄 수 있다.

테스트 격리

인수 테스트를 하다 보면 다른 테스트에 영향을 주는 일이 많이 발생한다.

 

이유는 RANDOM_PORT를 사용하면서 트랜잭션이 롤백되지 않기 때문이다.

  • 요청을 받는 쪽은 전혀 다른 스레드에서 새로운 트랜잭션으로 커밋하기 때문

이를 해결하려면 매 테스트마다 데이터를 초기화해야 한다.

초기화

DB 초기화를 하는 방식이 매우 어렵다고 한다. 아래의 방법을 고민할 수 있다.

  • Repository 활용 - 내부 구현에 많이 의존하게 된다.
  • SQL 실행 - SQL문에 의존하게 된다.
  • @DirtiesContext - 매번 Context를 재시작해서 시간이 매우 오래 걸린다.

두 방법은 모두 상황이 변동됨에 따라 테스트가 쉽게 깨질 수 있다는 단점이 있다. 그래서 바람직한 방법 중 하나로 실제 사용자 관점에서 API 요청을 통해서 초기화를 할 수도 있다.

 

하지만 이 방법도 테스트 중복이 많이 생기고 테스트 구성이 어렵다는 단점이 있다.

  • 테스트를 위해 별도의 Controller가 필요해질 수도 있다.

 

그래서 EntityManager를 활용한 방식을 추천한다.

해당 방식은 각 테스트 실행 시 테이블을 Truncate하고 AutoIncrement를 초기화한다.

테스트 코드 분리

위 테스트 코드는 250줄 가량 된다. 이러한 테스트 코드는 유지보수가 어려운 테스트 코드가 되서 발목을 잡을 수 있다.

 

하나의 테스트 코드는 작은 메서드 단위로 분리해서 수행한다.

이렇게 작성 메서드는 Steps에서 관리하면서, 여러 개의 테스트 코드에서 참조해서 사용할 수 있다.

테스트 메서드 네이밍

메서드 네이밍의 경우 메서드명에서 테스트 하고자 하는 시나리오를 충분히 담는다.

  • @DisplayName을 활용해서 상세하게 작성하는 방법도 있다.

외부 API 호출

인수 테스트 시 외부 API 호출을 어떻게 할까..? 사실 이 부분은 나도 실무를 하면서 고민이 많이 되었던 부분이다.

 

우아한 ATDD 강연에서는 실제 요청과 가짜 요청을 혼합해서 사용했다고 한다.

  • Github API의 경우 테스트 계정으로 실제 요청
    • 메인 서비스에서 매우 중요한 기능이라서 실제 요청을 사용한다.
    • 외부 서비스에 의존적인 테스트가 된다는 단점이 있다. (FIRST 원칙에 어긋남)
  • 결제 API 연동
    • 실제가 아닌 Fake 서비스에 요청하도록 구성
    • 외부 서비스의 변동 시 즉각적인 피드백이 어렵다는 단점

나의 경우에는 WireMock을 사용해서 외부 API의 컨텍스트를 Mocking한다.

  • https://wiremock.org/docs/junit-jupiter
  • Spring Cloud Contract를 사용하면 WireMock에 대한 다양한 기능을 제공한다.
    • MSA에서의 TDD 지원
    • Dynamic Port 기능 (yml 에서 활용)
    • ...

참고