테스트 피라미드
•
비용이 많이 드는 테스트는 지양하고 비용이 적게 드는 테스트를 만들어야 한다.
•
만드는 비용이 적고 유지보수하기 쉽고 빨리 실행되고 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다. 이 테스트는 하나의 ‘단위’(일반적으로 하나의 클래스)가 제대로 동작하는지 확인할 수 있는 테스트다.
•
테스트 피라미드는 테스트가 비싸질수록 테스트의 커버리지 목표는 낮게 잡아야 한다는 것을 보여준다.
•
‘단위 테스트’, ‘통합 테스트’, ‘시스템 테스트’의 정의는 맥락에 따라 다르다.
◦
단위 테스트는 피라미드 토대에 해당한다. 하나의 클래스를 인스턴스화하고 해당 클래스의 인터페이스를 통해 기능을 테스트한다. 의존되는 클래스들은 인스턴스화하지 않고 테스트하는 동안 목(mock)으로 대체된다.
◦
통합테스트는 여러 유닛을 인스턴스화하고 시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후 유닛들의 네트워크가 기대한대로 잘 동작하는 지 검증한다.
◦
시스템 테스트는 애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는 지 검증
단위 테스트로 도메인 엔티티 테스트하기
•
단위 테스트가 도메인 엔티티에 녹아 있는 비즈니스 규칙을 검증하기 적절한 방법이다.
AccountTest 코드
단위 테스트로 유스케이스 테스트하기
SendMoneyServiceTest 코드
•
행동-주도 개발(behavior driven develpment)에서 일반적으로 사용되는 방식대로 given/when/then 섹션으로 나눴다.
•
‘given’ 섹션에서는 출금 및 입금 Account를 인스턴스를 생성하고 적절한 상태로 만든다.
•
‘when’ 섹션에서는 유스케이스를 실행하기 위해 메서드를 호출한다.
•
‘then’ 섹션에서는 유스케이스를 실행하기 위해 sendMoney 메서드를 호출한다.
•
Mockito 라이브러리를 이용해 given 메서드의 목 객체를 생성할 수 있다. 목 객체에 대해 특정 메서드가 호출됐는지 검증할 수 있는 then 메서드도 제공한다.
•
테스트에서 어떤 상호작용을 검증하고 싶은지 신중하게 생각해야 한다. 모든 동작을 검증하는 대신 중요한 핵심만 골라 집중해서 테스트해야 한다. 클래스가 조금이라도 바뀔 때마다 테스트를 변경해야 한다. 테스트의 가치를 떨어진다.
•
단위 테스트이지만 의존성의 상호작용을 테스트하기 때문에 통합 테스트에 가깝다. 하지만 mock으로 작업하기 때문에 통합 테스트에 비해 만들고 유지보수하기 쉽다.
통합 테스트로 웹 어댑터 테스트하기
SendMoneyControllerTest 코드
•
웹 어댑터는 JSON 문자열 형태로 HTTP를 통해 입력받고 입력에 대한 유효성 검증을하고 유스케이스에서 사용할 수 있는 포맷으로 매핑하고 유스케이스에 전달한다.
•
유스케이스 결과를 JSON 매핑하고 HTTP 응답을 통해 클라이언트에 반환한다.
•
MockMvc 객체를 이용해 모킹했기 때문에 HTTP 프로토콜을 통해 테스트하지 않았다.
◦
하지만 프레임워크 테스트이기 때문에 잘 동작한다고 믿는다.
◦
프레임워크는 테스트할 필요가 없다.
•
하나의 웹 컨트롤러 클래스같지만 @WebMvcTest 애너테이션은 스프링이 특정 요청 경로, 자바와 JSON 매핑, HTTP 입력 검증 등 전체 객체 네트워크를 인스턴스화하도록 만든다. 그래서 통합 테스트이다.
•
웹 컨트롤러는 스프링 프레임워크와 강하게 묶여있어 격리된 상태로 테스트하기보다는 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.
통합 테스트로 영속성 어댑터 테스트하기
•
영속성 어댑터 테스트도 단위 테스트보다는 통합테스트를 적용하는 것이 합리적이다.
AccountPersistenceAdapterTest 코드
AccountPersistenceAdapterTest SQL
•
@DataJpaTest 애너테이션으로 데이터베이스 접근에 필요한 객체 네트워크를 인스턴스화 해야한다는 것을 스프링에게 알려준다.
•
@Import 애너테이션을 추가해서 특정 객체가 이 네트워크에 추가됐다는 것을 표현한다.
•
SQL 스크립트를 이용해서 데이터베이스를 특정 상태로 만든다. 그 다음 SQL 스크립트에서 설정한 상태값을 가지고 있는지 검증한다.
•
스프링에서는 기본적으로 인메모리(in-memory) 데이터베이스를 테스트에서 사용한다.
•
프로덕션 환경에서는 인메모리 데이터베이스를 사용하지 않는 경우가 많아 실제 데이터베이스에서 문제가발생 할 수 있다.
•
영속성 어댑터 테스트는 실제 데이터베이스를 대상으로 진행해야한다. Testcontainers 같은 라이브러리로 사용하여 데이터베이스를 도커 컨테이너에서 띄워서 사용한다.
시스템 테스트로 주요 경로 테스트하기
•
피라미드 최상단의 시스템 테스트로 전체 애플리케이션을 띄우고 API를 통해 요청 보내고 모든 계층이 조화롭게 잘 동작하는지 검증한다.
SendMoneySystemTest 코드
•
@SpringBootTest는 애플리케이션을 구성하는 모든 객체 네트워크를 띄우게 한다. 랜덤 포트도 이 애플리케이션을 띄우도록 한다.
•
웹 어댑터처럼 MockMvc를 이용해 요청을 보내는 것이 아니라 TestRestTemplate을 이용해서 요청을 보낸다.
•
실제 HTTP 통신을 하는 것처럼 실제 출력 어댑터도 이용한다.
•
시스템 테스트여도 언제나 서드파티 시스템을 실행해서 테스트할 수 있지는 않아 모킹을 해야할 때 도 있다. 육각형 아키텍처는 이런 경우 몇 개의 출력 포트 인터페이스만 모킹하면 이 문제를 해결할 수 있다.
•
헬퍼 메서드들은 여러가지 상태를 검증할 때 사용할 수 있는 도메인 특화 언어(domain-specific language, DSL)를 형성한다.
◦
시스템 테스트에서 유용하다. 실제 사용자를 잘 흉내내기 때문에 사용자 관점에서 애플리케이션을 검증할 수 있다.
◦
시스템 테스트는 여러 유스케이스를 결합했을 때 빛이난다.
얼마만큼의 테스트가 충분할까?
•
라인 커버러지(line coverage)는 테스트 성공을 측정하는 데 있어서 잘못된 지표다.
•
얼마나 마음 편하게 소프트웨어를 배포할 수 있으냐를 테스트 성공의 기준으로 잡아야한다.
우리가 만들어야 할 테스트를 정의하는 전략으로 시작하자.
•
육각형 아키텍처에서 사용하는 테스트 전략
◦
도메인 엔티티를 구현할 때는 단위 테스트로 커버하자
◦
유스케이스를 구현할 때는 단위 테스트로 커버하자
◦
어댑터를 구현할 때는 통합 테스트로 커버하자
◦
사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버하자
•
리팩토링할 때마다 테스트 코드로 변경해야 한다면 테스트는 테스트로서의 가치를 잃는다.
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
•
육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 깔끔히 분리한다.
•
도메인 로직은 단위 테스트로 어댑터는 통합 테스트로 처리하는 전략을 사용하는 것이다.
•
입출력 포트는 테스트에서 뚜렷한 모킹 지점이 된다.
•
포트 인터페이스가 더 적은 메서드를 제공할수록 어떤 메서드를 모킹해야할 지 덜 헷갈린다.