•
5장에서 다루는 내용
◦
목과 스텁 구분
◦
식별할 수 있는 동작과 구현 세부 사항 정의
◦
목과 테스트 취약성 간의 관계 이해
◦
리팩토링 내성 저하 없이 목 사용하기
•
테스트에서 목을 사용하는 것은 논란의 여지가 있는 주제다.
◦
어떤 사람들은 목이 훌륭한 도구이며 대부분의 테스트에 적용해야 한다고 주장한다.
◦
어떤 사람들은 목이 테스트 취약성을 초래하며 사용하지 말아야 한다고 주장한다.
•
이 장에서는 목이 취약한 테스트, 즉 리팩토링 내성이 약한 테스트를 초래하는 것을 살펴본다. 물론 목 사용이 바람직한 경우도 있다.
•
런던파는 테스트 대상 코드 조각을 서로 분리하고 불변 의존성(값, 값 객체)을 제외한 모든 의존성에 테스트 더블을 써서 격리하고자 한다.
•
고전파는 단위 테스트를 분리해서 병렬로 실행할 수 있게 하자고 한다. 테스트 간에 공유하는 의존성에 대해서만 테스트 더블을 사용한다.
1. 목과 스텁 구분
•
목은 테스트 대상 시스템과 그 협력자 사이의 상호 작용을 검사할 수 있는 테스트 더블이라고 했다. 또 다른 테스트 더블이 있는데 바로 스텁이다.
1. 테스트 더블 유형
•
테스트 더블은 모든 유형의 비운영용 가짜 의존성을 의미하는 포괄적인 용어다.
•
제라드 메스자로스에 의하면 테스트 더블에는 더미, 스텁, 스파이, 목, 페이크라는 다섯 가지 유형이 있다.
•
실제로는 목과 스텁의 두 가지 유형으로 나눌 수 있다.
◦
목 - 목, 스파이
▪
스파이는 수동으로 작성한다.
▪
목은 목 프레임워크의 도움을 받아 생성한다.
▪
목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다.
▪
이러한 상호작용은 테스트 대상 시스템이 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다.
◦
스텁 - 스텁, 더미, 페이크
▪
더미는 null 값이나 가짜 문자열과 같이 단순하고 하드코딩된 값이다.
▪
스텁은 더 정교하다. 시나리오마다 다른 값을 반환하게끔 설계된 완전한 의존성이다.
▪
페이크는 보통 존재하지 않는 의존성을 대체할 때 구현한다.
▪
스텁은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다.
▪
이러한 상호 작용은 테스트 대상 시스템이 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.
•
이메일 발송 동작을 수행하는 기능이 있다고 해보자. 이 기능에 있어서 외부 의존성은 이메일 발송과 데이터 검색이 있을 것이다.
•
이때 외부 의존성을 격리하기 위해 이메일 발송 기능은 외부로 나가는 상호작용이므로 목을 사용하고 데이터 검색은 내부로 들어오므로 스텁을 사용한다.
•
목과 스텁의 차이점에도 유의해야한다. 목은 테스트 대상 시스템과 관련 의존성 간의 상호 작용을 모방하고 검사하는 반면 스텁은 모방만 한다.
2. 도구로서의 목과 테스트 더블로서의 목
•
목은 종종 사람들에게 테스트 더블의 의미로서 사용되지만 그의 일부일 뿐이다. 목에는 또 다른 의미가 있는데, 목 라이브러리의 클래스도 목으로 참고할 수 있다. 이 클래스는 목을 만드는 도구지 목은 아니다.
•
이는 도구로서의 목이다. 이는 목과 스텁 두 가지 유형의 테스트 더블을 만들 수 있기 때문에 도구로서의 목과 테스트 더블로서의 목을 혼동하지 않는 것이 중요하다.
3. 스텁으로 상호 작용을 검증하지 말라
•
목은 SUT에서 관련 의존성으로 나가는 상호 작용을 모방하고 검사한다. 스텁은 내부로 들어오는 상호 작용만 모방하지 검사는 수행하지 않는다.
•
이 두 가지 차이는 스텁과의 상호 작용을 검증하지 말라는 지침에서 비롯된다.
•
SUT에서 스텁으로부터 호출해오는 값은 최종 결과가 아니다. 이러한 호출은 최종 결과를 산출하기 위한 수단일 뿐이다.
◦
즉 스텁은 SUT가 결과를 생성할 수 있도록 입력을 제공한다. 스텁에게 의존하는 행위는 취약한 테스트를 야기하는 안티 패턴이다.
•
만약 최종 결과가 아닌 사항을 검증하는 된다면 이는 과잉 명세가 발생한 것이다. 보통 상호 작용을 검사할 때 가장 흔하게 발생한다.
•
스텁과의 상호 작용을 확인하는 것은 쉽게 발견할 수 있는 결함이다. 테스트가 스텁과의 상호 작용을 해서는 안 된다.
4. 목과 스텁 함께 쓰기
•
때로는 목과 스텁의 특성을 모두 가지고 있는 테스트 더블이 필요할 때가 있다.
•
예를 들면 다른 객체가 있겠다. 이는 우리에게 데이터를 제공하기도 이 객체에 요청하기도 하므로 두 특성을 가지고 있다.
•
그러나 스텁과의 상호작용은 검증하지 말라. 테스트 더블은 목이면서 스텁이지만 여전히 목이라고 부르지 스텁이라고 부르지 않는다.
5. 목과 스텁은 명령과 조회에 어떻게 관련되어 있는가?
•
명령은 사이드 이펙트를 일으키고 어떤 값도 반환하지 않는 메소드다.
◦
사이드 이펙트의 예로는 객체 상태 변경, 파일 시스템 내 파일 변경 등이 있다.
•
조회는 그 반대로, 사이드 이펙트가 없고 값을 반환한다.
•
명령을 대체하는 테스트 대역은 목이다. 마찬가지로 조회를 대체하는 테스트 대역은 스텁이다.
•
즉, 사이드 이펙트가 있는 명령은 목을 이용해서 대체한다. 값을 반환한다면 조회이므로 스텁을 이용해서 대체한다.
2. 식별할 수 있는 동작과 구현 세부 사항
•
테스트 취약성은 좋은 단위 테스트의 두 번째 특성인 리팩토링 내성에 해당한다.
•
이런 리팩토링 내성을 키우기 위해선 코드가 생성하는 최종 결과를 검증하며 구현 세부 사항과 테스트를 가능한 떨어뜨리는 것 뿐이다.
•
즉 테스트는 ‘어떻게’가 아니라 ‘무엇’에 중점을 두어야 한다. 그렇다면 구현 세부 사항은 정확히 무엇이며 식별할 수 있는 동작과 어떻게 다른 걸까?
1. 식별할 수 있는 동작은 공개 API와 다르다.
•
모든 제품 코드는 2차원으로 분류할 수 있다.
◦
공개 API 또는 비공개 API
◦
식별할 수 있는 동작 또는 구현 세부 사항
•
코드가 시스템의 식별할 수 있는 동작이라면 다음 중 하나를 수행해야 한다.
◦
비즈니스 로직을 수행하는 데 도움이 되는 연산을 노출한다. 연산은 계산을 수행하거나 사이드 이펙트를 초래하거나 둘 다 하는 메서드다.
◦
비즈니스 로직을 수행하는 데 도움이 되는 상태를 노출하라. 상태는 시스템의 현재 상태다.
•
구현 세부 사항은 이 두 가지 중 아무것도 하지 않는다.
•
코드가 식별할 수 있는 동작인지 여부는 해당 클라이언트가 누구인즈 그리고 해당 클라이언트의 모굪가 무엇인지에 달려 있다. 식별할 수 있는 동작이 되려면 이러한 목표 중 하나라도 직접적인 관계가 있어야 한다.
•
잘 설계된 API에서 식별할 수 있는 동작은 공개 API와 일치하는 반면, 모든 구현 세부 사항은 비공개 API 뒤에 숨어있다.
•
물론 잘 설계된이다. 세상엔 이상적이지 못한 코드가 더 많다.
2. 구현 세부 사항 유출: 연산의 예
•
구현 세부 사항이 공개 API로 유출되는 코드의 예를 살펴보자.
@Getter
@Setter
public class User {
public String name;
public String normalizeName(String name) {
...
}
}
public class UserController {
public void renameUser(int userId, string newName) {
User user = getUserFromDatabase(userId);
string normalizedName = user.normalizeName(newName);
user.name = normalizedName;
saveUserToDatabase(user);
}
}
Java
복사
•
짐작했듯이 이 메서드의 관심은 유저의 이름을 변경하는 것이다. 그렇다면 이 API가 적절히 설계되지 않은 이유는 뭘까?
•
클래스 API를 잘 설계하려면 해당 멤버가 식별할 수 있는 동작이 되게 해야한다. 이는 다음과 같은 속성을 가져야함을 앞서 살펴봤다.
◦
클라이언트가 목표를 달성하는 데 도움이 되는 작업을 노출하라.
◦
클라이언트가 목표를 달성하는 데 도움이 되는 상태를 노출하라.
•
UserController가 클라이언트라고 가정해보자. 클라이언트는 renameUser를 이용해서 이름을 변경하고 싶다. 즉 관심은 유저의 이름을 변경하는 것이다.
•
여기서 UserController가 목표를 달성하는 데 도움이 되는 작업은 Setter다.
•
normlizeName 메소드도 작업이지만 클라이언트의 관심에 직결되지 않는 구현 세부 사항이다. 따라서 이는 비공개 API로 변경하는 것이 적합하다.
•
구현 세부 사항이 노출되었는지 확인하는 방법은 단일한 목표를 달성하고자 클래스에서 호출해야 하는 연산의 수가 1줄 이상이라면 구현 세부 사항이 유출되었을 가능성이 있다.
•
이상적으로는 단일 연산으로 목표를 달성해야 한다.
3. 잘 설계된 API와 캡슐화
•
캡슐화는 불변성 위반이라고도 하는 모순을 방지하는 조치다. 불변성은 항상 참이어야 하는 조건이다.
•
구현 세부 사항을 노출하면 불변성 위반을 가져온다.
•
장기적으로 유지 보수에 있어서는 캡슐화가 중요하다. 프로그래밍의 복잡도 때문이다. 계속해서 증가하는 코드 복잡도에 대처할 수 있는 방법은 실질적으로 캡슐화말고는 없다.
•
코드 API가 해당 코드로 할 수 있는 것과 없는 것을 알려주지 않으면 코드가 변경됐을 때 모순이 생기지 않도록 많은 정보를 알고 있어야 한다. 이는 프로그래밍 프로세스에 정신적 부담을 증대한다.
•
캡슐화를 올바르게 유지해 코드베이스에서 잘못할 수 있는 옵션조차 제공하지 않도록 하는 것이 좋다. 캡슐화는 궁극적으로 단위 테스트와 동일한 목표를 지향한다. 즉 생산성을 유지하는 것이다.
•
묻지 말고 말하게 하라라는 원칙이 있다. 이는 데이터를 연산 기능과 결합하는 것을 의미한다.
•
따라서 좋은 테스트 코드를 만들고 유지 보수성을 향상시키고 싶다면 캡슐화를 항상 잘 해두어야 한다. 정리한다면 다음과 같다.
◦
구현 세부 사항을 숨기면 클라이언트의 시야에서 클래스 내부를 숨길 수 있기 때문에 내부가 오염될 위험이 적다.
◦
데이터와 연산을 결합하면 해당 연산이 해당 연산이 클래스의 불변성을 위반하지 않도록 할 수 있다.
4. 구현 세부 사항 유출: 상태의 예
•
불필요한 멤버 변수의 공개를 막으라는 의미다.
•
객체의 상태는 클라이언트에게 노출되어서는 안 된다. 따라서 상태를 비공개로 변경하는 것이 좋다.
◦
이처럼 구현 세부 사항을 모두 비공개로 바꾸면 테스트에서는 식별할 수 있는 동작을 검증하는 것 외에는 아무런 선택지가 없다.
•
즉, 테스트가 식별할 수 있는 동작만 검증하도록 강제할 수 있고 덕분에 테스트 코드의 리팩토링 내성이 증가한다.
•
정리하자면 공개, 비공개 여부에 따른 식별할 수 있는 동작과 구현 세부 사항의 관계가 리팩토링 내성에 끼치는 영향은 다음과 같다.
식별할 수 있는 동작 | 구현 세부 사항 | |
공개 | 좋다. | 나쁘다. |
비공개 | 해당 없다. | 좋다. |
3. 목과 테스트 취약성의 관계
•
이 절에서는 헥사고날 아키텍처, 내부 통신과 외부 통신의 차이점 그리고 목과 테스트 취약성 간의 관계를 알아본다.
1. 헥사고날 아키텍처 정의
•
전형적인 애플리케이션의 형태는 도메인과 애플리케이션 서비스라는 두 계층으로 구성된다.
•
도메인 계층은 애플리케이션의 중심부이기 때문에 중앙에 위치한다. 여긴 비즈니스 로직이 포함되어 있다.
◦
도메인 계층과 해당 비즈니스 로직은 이 애플리에키션이 다른 애플리케이션과 차별화하고 조직의 경쟁력을 향상시키는 역할을 수행한다.
•
애플리케이션 서비스 게층은 도메인 계층을 감싸고 있으며 외부 환경과의 통신을 담당한다.
◦
데이터베이스를 조회한다.
◦
해당 인스턴스에 연산을 호출한다.
◦
결과를 다시 데이터베이스에 저장한다.
•
애플리케이션 계층과 도메인 계층의 조합은 육각형을 형성하며 이 육각형은 애플리케이션을 나타낸다. 이는 다른 애플리케이션과 소통할 수 있고 다른 것들오 육각형으로 나타난다.
•
헥사고날 아키텍처의 목적은 세 가지 중요한 지침을 강조하는 것이다.
◦
도메인 계층과 애플리케이션 서비스 계층 간의 관심사 분리
▪
비즈니스 로직이 위치한 도메인 계층은 가장 중요한 부분이다. 따라서 도메인 계층은 비즈니스 로직에 대해서만 책임을 져야하며 다른 책임에서는 제외되어야 한다.
▪
반대로 애플리케이션 서비스에는 어떤 비즈니스 로직도 있으면 안 된다.
◦
애플리케이션 내부 통신
▪
헥사고날 아키텍처는 애플리케이션 서비스 게ㅖ층에서 도메인 계층으로 흐르는 단방향 의존성 흐름을 규정한다.
▪
도메인 계층 내부 클래스는 도메인 계층 내부 클래스끼리만 의존하고 애플리케이션 서비스 계층에는 의존하지 않아야 한다.
▪
도메인 계층은 외부 환경에서 완전히 격리되어야 한다.
◦
애플리케이션 간의 통신
▪
외부 애플리케이션은 애플리케이션 서비스 계층에 있는 공통 인터페이스를 통해 연결된다.
▪
아무도 도메인 계층에 접근할 수 없다.
•
식별할 수 있는 동작은 바깥 계층에서 안쪽으로 흐른다.
2. 시스템 내부 통신과 시스템 외부 통신
•
일반적인 애플리케이션에는 시스템 내부 통신과 시스템 간 통신이 있다.
•
시스템 태부 통신은 애플리케이션 내 클래스 간의 통신이다. 시스템 간 통신은 애플리케이션이 다른 애플리케이션과 통신하는 것을 말한다.
◦
시스템 내부 통신은 구현 세부 사항이고 시스템 간 통신을 그렇지 않다.
◦
연산을 처리하기 위한 도메인 클래스 간의 협력은 식별할 수 있는 동작이 아니므로 시스템 내부 통신은 구현 세부 사항에 해당한다.
◦
이러한 협력은 클라이언트의 목표와 직접적인 관계가 없다. 따라서 이러한 협력과 결합되면 테스트는 취약해진다. 즉 깨지기 쉬워진다.
•
시스템 외부 환경과 통신하는 방식은 전체적으로 해당 시스템의 식별할 수 있는 동작을 나타낸다.
•
목을 사용하면 시스템과 외부 애플리케이션 간의 통신 패턴을 확인할 때 좋다.
•
반대로 시스템 내 클래스 간의 통신을 검증하는 데 목을 사용하면 테스트가 구현 세부 사항과 결합되며 리팩터링 내성이 약해진다.
•
시스템 내부 통신, 즉 도메인 간의 연산에서 목을 사용하면 리팩토링 내성이 약해질 것이다.
4. 단위 테스트의 고전파와 런던파 재고
•
이제 알겠다. 런던파를 따라 목을 무분별하게 사용하면 구현 세부 사항에 테스트가 강하게 결합되어 리팩토링 내성을 잃게 된다.
•
고전파는 테스트 간에 공유하는 의존성만 테스트 더블로 대체하자고 하므로 이 문제에 훨씬 유리하다.
•
그러나 고전파 역시 시스템 간 통신에 대한 처리에 이상적이지는 않다. 고전파도 목 사용을 지나치게 장려하는 편이다.
1. 모든 프로세스 외부 의존성을 목으로 해야 하는 것은 아니다.
•
이를 설명하기 전에 의존성 유형부터 다시 훑어보자.
◦
공유 의존성: 테스트 간에 공유하는 의존성(제품 코드가 아니다)
◦
프로세스 외부 의존성: 프로그램의 실행 프로세스 외에 다른 프로세스를 점유하는 의존성(데이터베이스, 메시지 버스, SMTP 서비스 등)
◦
비공개 의존성: 공유하지 않는 모든 의존성
•
고전파는 공유 의존성을 피할 것을 권한다. 테스트끼리 영향을 끼치면 이는 병렬 처리가 불가능하기 때문이다.
◦
테스트를 병렬적, 순차적으로 실행할 수 있는 것을 테스트 격리라고 부른다.
•
완전히 통제권을 가지고 있는, 즉 하위 호환성 요구사항(변경이 불가능한)이 존재하지 않는다면 구현 세부 사항이 된다.
•
이런 프로세스 외부 의존성에 목을 사용하면 깨지기 쉬운 테스트로 이어진다.
•
데이터베이스의 테이블을 변경하거나 프로시저를 수정했다고 테스트에 빨간불이 들어와서는 안 된다. 데이터베이스와 애플리케이션은 하나의 시스템으로 취급해야 한다.
•
이는 어렵다. 피드백 속도를 저하시키지 않고 이러한 의존성으로 어떻게 테스트할까, 이는 6, 7장에서 설명한다.
2. 목을 사용한 동작 검증
•
종종 목이 동작을 검증한다고 한다. 하지만 대부분의 경우 그렇지 않다. 목표를 달성하고자 각 개별 클래스가 이웃 클래스와 소통하는 것은 구현 세부 사항이다.
•
이러한 세부 수준은 너무 세밀하다. 중요한 것은 클라이언트의 목표로 거슬러 올라갈 수 있는 즉 식별할 수 있는 동작이 되는 단위다.
•
목은 애플리케이션의 경계를 넘나드는 상호 작용을 검증할 때와 이러한 상호 작용의 사이드 이펙트가 외부 환경에서 보일 때만 동작과 관련이 있다.
요약
•
테스트 더블은 테스트에서 비제품 가짜 의존성의 모의 유형을 설명하는 포괄적인 용어다.
◦
테스트 더블에는 더미, 스텁, 스파이, 목, 페이크 등이 있는데, 이는 다시 목과 스텁이라는 두 유형으로 분류할 수 있다.
◦
스파이는 기능적으로 목과 같고 더미와 페이크는 스텁과 같은 역할을 한다.
•
목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 스텁은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다.
•
Mock은 테스트 더블을 만드는 데 사용할 수 있는 목 라이브러리의 클래스다.
•
스텁과의 상호 작용을 검증하면 취약한 테스트로 이어진다. 이는 최종 결과가 아니며 구현 세부 사항이기 때문이다.
•
CQRS 원칙에 따르면 모든 메소드가 명령 또는 조회 중 하나다. 명령을 대체하는 테스트 더블은 목이며 조회를 대체하는 테스트 더블은 스텁이다.
•
모든 코드는 공개 API인지 비공개 API인지, 식별할 수 있는 동작인지 구현 세부 사항인지라는 두 차원으로 분류할 수 있다.
◦
코드의 공개성은 private, public, protected, default와 같은 접근 제한자에 의해 제어된다.
◦
동작과 구현 세부 사항인지는 다음 요구 사항 중 하나라도 충족하면 식별할 수 있는 동작이다.
▪
클라이언트가 목표를 달성하는 데 도움이 되는 연산을 노출한다. 연산은 계산을 수행하거나 사이드 이팩트를 초래하거나 또는 둘 다 하는 메소드다.
▪
클라이언트가 목표를 달성하는 데 도움이 되는 상태를 노출한다. 상태는 시스템의 현재 상태다.
•
잘 설계된 코드는 식별할 수 있는 동작과 공개 API가 일치하고 구현 세부 사항이 비공개 API로써 존재하는 코드다.
◦
공개 API가 식별할 수 있는 동작 이외의 구현 세부 사항들을 포함한다면 이는 깨질 위험이 있다.
•
캡슐화는 코드를 불변성 위반으로부터 보호하는 행위다. 클라이언트는 구현 세부 사항을 사용해 코드의 불변성을 우회할 수 있기 때문에 구현 세부 사항을 노출하면 캡슐화가 위반되는 경우가 종종 있다.
•
헥사고날 아키텍처는 상호 작용하는 애플리케이션들의 집합이고 각 애플리케이션은 육각형으로 표시한다. 각 육각형은 도메인, 애플리케이션 서비스 계층으로 나뉜다.
•
헥사고날 아키텍처는 다음과 같은 세 가지 관점을 강조한다.
◦
도메인과 애플리케이션 서비스 계층 간의 영향을 분리하라. 도메인 계층은 비즈니스 로직을 책임져야 하고 애플리케이션 서비스는 도메인 계층과 외부 애플리케이션 간의 작업을 조율한다.
◦
애플리케이션 서비스 계층에서 도메인 계층으로의 단방향 의존성 흐름을 가져야 한다. 도메인 계층 내 클래스는 서로에게만 의존해야 하고 애플리케이션 서비스 계층의 클래스에 의존해서는 안 된다.
▪
User 클래스가 UserController를 갖다 쓰면 될까..?
◦
외부 애플리케이션은 애플리케이션 서비스 계층이 유지하는 공통 인터페이스를 통해서 연결된다. 아무도 도메인 계층에 직접 액세스할 수 없다.
•
애플리케이션에는 시스템 내부 통신과 시스템 간 통신이라는 두 가지 통신 유형이 있다.
◦
시스템 내부 통신은 애플리케이션 내 클래스 간의 통신이다. 시스템 간 통신은 애플리케이션이 외부 애플리케이션과 통신할 때를 말한다.
•
시스템 내 통신은 구현 세부 사항이다. 애플리케이션을 통해서만 접근할 수 있는 외부 시스템을 제외하고 시스템 간 통신은 식별할 수 있는 동작이다.
◦
애플리케이션을 통해서만 접근할 수 있는 외부 시스템과의 상호 작용도 구현 세부 사항인데, 이 상호 작용으로 인한 사이드 이펙트를 외부에서 확인하거나 강제하지 않기 때문이다.
•
시스템 내 통신을 검증하고자 목을 사용한다면 취약한 테스트로 이어진다. 따라서 시스템 간 통신과 해당 통신의 사이드 이펙트가 외부환경에서 보일 때만 목을 사용하는 것이 현명하다.