테스트 대역의 유형은 목, 스텁 두가지로 나눌 수 있다.
목: 외부로 나가는 상호 작용을 모방 (command)
스텁: 내부로 들어오는 상호 작용을 모방 (query)
스텁을 검증하는 것은 안티패턴이다.
SUT에서 스텁의 호출은 SUT가 생성하는 최종 결과가 아니다. 최종 결과를 산출하기 위한 수단일 뿐이다. (구현 세부 사항)
구현 세부 사항에 의존하게 되면 깨지기 쉬운 테스트가 된다.
결과가 올바르다면 SUT가 최종 결과를 어떻게 생성하는지는 중요하지 않다.
최종 결과가 아닌 사항을 검증하는 것을 과잉 명세라고 한다.
명령(command)을 대체하는 테스트 대역은 목이고, 조회(query)를 대체하는 테스트 대역은 스텁이다.
테스트 코드와 구현 코드의 강결합을 피하는 방법
코드가 생성하는 최종 결과(식별할 수 있는 동작) 를 검증하고 구현 세부 사항은 신경쓰지 않는 것
테스트는 "어떻게"가 아니라 "무엇"에 중점을 둬야 한다.
시스템의 공개 API는 식별할 수 있는 동작과 일치해야 하며, 모든 구현 세부 사항은 클라이언트 눈에 보이지 않아야 한다.
단일한 목표를 달성하고자 클래스에서 호출해야 하는 연산의 수가 1보다 크면 해당 클래스에서 구현 세부 사항을 유출할 가능성이 있다.
@AllArgsConstructor
public class User {
private Long id;
public String name;
public String normalizeName(String name) {
String result = name.trim();
if (result.length() > 50) {
return result.substring(0, 50);
}
return result;
}
}
normalizeName에서 이름이 50자가 넘으면 안된다는 불변성이 존재한다.
하지만 위 코드에서는 name의 접근 제어자가 pulbic으로 외부에 공개되어 있기 때문에 클라이언트는 이름을 먼저 정규화하지 않고 name을 변경할 수 있다. (불변성 위반)
계속해서 증가하는 코드 복잡도에 대처할 수 있는 방법은 실질적으로 캡슐화 밖에 없다.
코드 API가 해당 코드로 할 수 있는 것과 할 수 없는 것을 알려주지 않으면 코드가 변경 되었을 때 불변성이 깨지지 않도록 많은 정보를 염두에 둬야 한다.
개발자가 항상 옳게만 한다고 믿을수는 없으므로 실수할 가능성을 최대한 없애자.
가장 좋은 방법이 캡슐화를 하여 실수할 수 있는 여지를 차단하는 것이다.
묻지 말고 시켜라(tell don't ask)
도메인과 애플리케이션 서비스 두 계층으로 구성된다.
도메인 계층
도메인 계층이 도표의 중앙에 위치한다.
도메인 계층에 애플리케이션의 필수 기능으로 비즈니스 로직이 포함되어 있다.
도몌인 계층과 비즈니스 로직은 애플리케이션을 다른 애플리케이션과 차별화하고 조직의 경쟁력을 향상시킨다.
애플리케이션 서비스 계층
도메인 계층 위에 있으며, 외부 환경과의 통신을 조정한다.
도메인 클래스와 프로세스 외부 의존성 간의 작업을 조정한다.
여러 육각형이 서로 소통하면서 육각형 아키텍처를 구성한다.
도메인 계층의 관심사와 애플리케이션 서비스 계층의 관심사를 분리하는 것이 핵심이다.
도메인은 비즈니스 로직에 대해서만 책임을 져야하며,
외부와 통신하거나 데이터베이스 접근 같은 책임은 애플리케이션 서비스계층만이 져야한다.
육각형 아키텍처는 애플리케이션 서비스 계층에서 도메인 계층 한방향으로만 의존성이 흐른다.
의존성이 한뱡향으로만 흐를 수 있는 이유는 관심사를 분리하였기 때문인데, 관심사를 명확히 분리하게 되면 애플리케이션 서비스 계층은 도메인 계층에 대해 의존하고, 알고 있지만, 도메인 계층은 애플리케이션 서비스 계층을 의존하고 있지 않고, 전혀 알지 못한다. 즉, 도메인 계층은 외부 환경에서 완전히 격리되어 있어야 한다.
외부 애플리케이션은 도메인 계층에 직접 접근할 수 없다.
육각형의 각 면은 애플리케이션 내외부 연결을 나타낸다.
각 계층의 API를 잘 설계하면(캡슐화) 달성하는 목표는 같지만 서로 다른 수준에서 동작을 검증하게 된다.
애플리케이션 서비스를 다루는 테스트는 해당 서비스가 외부 클라이언트가 목표를 어떻게 이루는지 확인한다.
도메인 클래스 테스트는 그 하위 목표를 검증한다.
공유 의존성: 테스트 간에 공유하는 의존성
프로세스 외부 의존성: 프로그램의 실행 프로세스 외에 다른 프로세스를 점유하는 의존성 (데이터 베이스, 메시지 버스, SMTP 서비스 등)
비공개 의존성: 공유하지 않는 모든 의존성
공유 의존성은 피해야 한다. 모든 테스트는 독립적으로 실행되어야 하기 때문이다. (테스트 격리)
애플리케이션에만 사용되고, 어떤 외부 시스템도 특정 시스템에 접근할 수 없는 애플리케이션이 완전히 통제권을 가진 프로세스 외부 의존성은 목을 사용할 필요가 없다. 오히려 목을 사용할 경우 깨지기 쉬운 테스트가 될 수 있다. 이러한 외부 시스템은 애플리케이션과 하나의 시스템으로 취급해야 한다.
대표적인 예가 애플리케이션에서만 사용되는 데이터베이스다. 어떤 외부 시스템도 접근할 수 없어서 기존 기능을 손상시키지 않는 한 시스템과 애플리케이션 데이터베이스 간의 통신 패턴을 원하는 대로 수정할 수 있다.
클라이언트의 시야에서 완전히 숨어있다.
이러한 의존성을 목으로 대체하게 되면 깨지기 쉬운 테스트가 될 수 있다.
테이블 분할, 매개변수 타입 변경 등을 할 때마다 테스트가 실패하게 된다.
대부분의 경우 목이 동작을 검증할 수 없다.
목표를 달성하고자 각 개별 클래스가 이웃 클래스와 소통하는 방식은 식별할 수 있는 동작과는 아무런 관계가 없다. 즉, 구현 세부 사항이다.
중요한 것은 클라이언트 목표로 거슬러 올라갈 수 있는 동작이다.
클라이언트에게 중요한 것은 도움뿐이며 그 도움을 요청함으로써 얻는 식별할 수 있는 동작이다.
목은 애플리케이션 경계를 넘나드는 상호 작용을 검증할때와 이러한 상호 작용의 부작용이 외부 환경에서 보일 때만 동작과 관련이 있다.