////
Search
Duplicate
🪛

5. 목과 테스트 취약성

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를 갖다 쓰면 될까..?
외부 애플리케이션은 애플리케이션 서비스 계층이 유지하는 공통 인터페이스를 통해서 연결된다. 아무도 도메인 계층에 직접 액세스할 수 없다.
애플리케이션에는 시스템 내부 통신과 시스템 간 통신이라는 두 가지 통신 유형이 있다.
시스템 내부 통신은 애플리케이션 내 클래스 간의 통신이다. 시스템 간 통신은 애플리케이션이 외부 애플리케이션과 통신할 때를 말한다.
시스템 내 통신은 구현 세부 사항이다. 애플리케이션을 통해서만 접근할 수 있는 외부 시스템을 제외하고 시스템 간 통신은 식별할 수 있는 동작이다.
애플리케이션을 통해서만 접근할 수 있는 외부 시스템과의 상호 작용도 구현 세부 사항인데, 이 상호 작용으로 인한 사이드 이펙트를 외부에서 확인하거나 강제하지 않기 때문이다.
시스템 내 통신을 검증하고자 목을 사용한다면 취약한 테스트로 이어진다. 따라서 시스템 간 통신과 해당 통신의 사이드 이펙트가 외부환경에서 보일 때만 목을 사용하는 것이 현명하다.