2장에서 다루는 내용
- 단위 테스트란?
- 공유 의존성, 비공개 의존성, 휘발성 의존성 간의 차이점
- 단위 테스트의 두 분파: 고전파와 런던파
- 단위 테스트, 통합 테스트, 엔드 투 엔드 테스트의 차이점
2.1 ‘단위 테스트’의 정의
단위 테스트에는 많은 정의가 있는데 쉽게 다음과 같이 세 가지 속성이 있다.
•
작은 코드 조각(단위)을 검증하고
•
빠르게 수행하고
•
격리된 방식으로 처리하는 자동화된 테스트다. → 대중의 의견이 크게 다른 속성
◦
격리 문제는 고전파와 런던파를 구분할 수 있게 해주는 근원적 차이
단위 테스트의 고전파와 런던파
고전적 접근법은 ‘디트로이트(Detroit)’라고도 하며 단위 테스트에 대한 고전주의적 접근법
런던 스타일은 때때로 ‘목 추종자(mockist)’로 표현
2.1.1 격리 문제에 대한 런던파의 접근
•
런던파에서는 테스트 대상 시스템을 협력자(Collaborator)에게서 격리하는 것을 말한다.
•
하나의 클래스가 다른 클래스 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야한다.
◦
외부 영향과 분리해서 테스트 대상 클래스에만 집중 가능
→ 테스트가 실패하면 코드베이스의 어느 부분이 문제인지 알 수 있다.
◦
객체 그래프를 분할 할 수 있다.
→ 모든 클래스가 각각 직접적인 의존성을 갖고 있으며 또 그 의존성이 또 다른 의존성을 갖고 있는 형태로 그래프가 상당히 복잡해 질 수 있다.
그림 2.1 테스트 대상 시스템의 의존성을 테스트 대역으로 대체하면, 테스트 대상 시스템만 검증하는데 집중할 수 있을 뿐만 아니라 규모가 큰 상호 연결 객체 그래프를 분할할 수 있다.
•
따라서, 테스트 대역을 사용하면 객체 그래프를 다시 만들거나 직접적인 의존성을 대체할 수 있고, 전체 단위 테스트 스위트를 간단한 구조로 할 수 있다.
그림 2.2 테스트 대상 클래스를 의존성에서 분리하면 단순한 테스트 스위트 구조(제품 코드의 각 클래스에 대해 테스트 클래스가 하나씩 있는 구조)를 확립하는 데 도움이 된다.
•
예제 2.1 - 고전적인 스타일로 작성된 상점에 재고가 충분히 있을 때만 구매가 성공하는지 검증하는 두가지 테스트
◦
의존성과 테스트 대상 시스템을 모두 준비해야한다.
•
예제 2.2 - 고전적인 스타일에서 Store 인스턴스를 목(Mock) 으로 대체하여 런던 스타일로 작성한다.
테스트 대상 시스템(SUT, System Under Test)
테스트 대상 메서드(MUT, Method Under Test) : 테스트에서 호출한 SUT의 메서드
MUT와 SUT는 흔히 동의어로 사용하지만, 일반적으로는 MUT는 메서드를 가리키는 데 반해 SUT는 클래스 전체를 가리킨다.
2.1.2 격리 문제에 대한 고전파의 접근
•
런던 스타일은 목으로 테스트 대상 코드 조각을 분리해서 격리 요구사항에 다가간다.
→ 이 관점은 무엇이 작은 코드조각에 해당하는지에 대한 견해에도 영향을 미친다.
•
단위 테스트의 첫 번째 속성에도 다양한 해석이 가능하다. 작은 코드 조각은 얼마나 작아야 하는가?
◦
각각의 모든 클래스를 격리해야 한다면 테스트 대상 코드 조각은 단일 클래스이거나 해당 클래스 내부의 메서드여야 한다.
•
고전적인 방법에서는 코드를 꼭 격리하는 방식으로 테스트해야 하는 것은 아니라는 입장이다.
•
대신 단위 테스트는 서로 격리해서 실행해야 한다.
→ 테스트를 어떤 순서로든 가장 적합한 방식으로 실행할 수 있으며 서로의 결과에 영향을 미치지 않는다.
테스트 격리 실행 예시
어떤 테스트 A가 준비 단계에서 데이터베이스에서 고객을 생성할 수 있고,
이 테스트가 실행되기 전에 다른 테스트 B의 준비 단계에서 고객을 삭제할 수도 있다.
이 테스트를 병렬로 실행하면 첫 번째 테스트가 실패하는데,
이는 코드가 문제가 있어서가 아닌 두 번째 테스트의 간섭 때문이다.
공유 의존성은 테스트 간에 공유되고 서로의 결과에 영향을 미칠 수 있는 수단을 제공하는 의존성이다.
공유 의존성의 전형적인 예는 정적 가변 필드다. 이러한 필드의 변경 사항은 동일한 프로세스 내에서 실행되는 모든 단위 테스트에서 볼 수 있다. 데이터베이스도 공유 의존성의 전형적인 예가 될 수 있다.
•
격리 문제에 이러한 해석에는 목과 기타 테스트 대역을 적당히 섞어 쓰려는 견해가 있다. 테스트 대역을 사용할 수 있지만, 테스트 간에 공유 상태를 일으키는 의존성에 대해서만 사용한다.
그림 2.3 단위 테스트를 서로 격리하는 것은 테스트 대상 클래스에서 공유 의존성만 격리하는 것을 의미한다. 비공개 의존성은 그대로 둘 수 있다.
•
공유 의존성은 테스트 대상 클래스 간이 아니라 단위 테스트 간에 공유한다.
◦
싱글턴 의존성은 각 테스트에서 새 인스턴스를 만들 수 있기만 하면 공유되지 않는다.
◦
코드에는 싱글턴 인스턴스가 단 하나만 있지만, 테스트는 이 패턴을 따르지 않고 재사용하지도 않는다.
◦
이러한 의존성은 비공개 의존성인 것이다.
•
예를 들면 설정 클래스는 일반적으로 한 개뿐이며, 모든 코드에서 이 인스턴스를 재사용한다.
◦
그러나 생성자 등을 통해 다른 모든 의존성이 테스트 대상 시스템에 주입되면 각 테스트에서 새 인스턴스를 만들 수 있다.
→ 테스트 스위트 전체에서 단일 인스턴스를 유지할 필요는 없다.
◦
하지만 새 파일 시스템이나 데이터베이스를 만들 수 없으니 테스트 간에 공유되거나 테스트 대역으로 대체돼야 한다.
•
공유 의존성을 대체하는 또 다른 이유는 테스트 실행 속도를 높이는데 있다.
◦
공유 의존성은 거의 실행 프로세스 외부에 있어 데이터베이스나 파일 시스템 등의 공유 의존성에 대한 호출은 비공개 의존성에 대한 호출보다 더 오래 걸린다.
◦
이는 단위 테스트 두 번째 속성으로 빨리 실행해야하는 필요성으로 이어진다.
2.2 단위 테스트의 런던파와 고전파
런던파는 테스트 대상 시스템에서 협력자를 격리하는 것으로 보는 반면, 고전파는 단위 테스트끼리 격리하는 것으로 본다.
•
두 개의 분파는 크게 세 가지 주요 주제에 대해 의견 차이가 있다.
◦
격리 요구 사항
◦
테스트 대상 코드 조각의 구성 요소
◦
의존성 처리
2.2.1 고전파와 런던파가 의존성을 다루는 방법
•
런던파는 테스트에서 일부 의존성을 그대로 사용할 수 있도록 하고 있다.
◦
불변 객체는 교체하지 않아도 된다.
◦
예제 2.2에서 보았듯 Product 인스턴스를 목으로 바꾸지 않고 실제 객체를 사용했다.
◦
Customer의 두 가지 의존성 중 Store만 시간에 따라 변할 수 있는 내부 상태를 포함하고 있고
Product는 불변이다. 따라서 Store 인스턴스만 목으로 교체했다.
그림 2.4 의존성 계층. 고전파에서는 공유 의존성을 테스트 대역으로 교체한다. 런던파에서는 변경 가능한 한 비공개 의존성도 테스트 대역으로 교체할 수 있다.
•
모든 프로세스 외부 의존성이 공유 의존성의 범주에 속하는 것은 아니다. 공유 의존성은 거의 항상 프로세스 외부에 있지만 반대로 프로세스 외부에 있다고 항상 공유 의존성은 아니다.
◦
공유이지만 프로세스 외부가 아닌 의존성의 예로 싱글턴이나 클래스의 정적 필드 등이 있다.
◦
데이터베이스는 공유이면서 프로세스 외부에 있다. 주 프로세스 외부에 상주하며 변경이 가능하다.
◦
읽기 전용 API는 프로세스 외부에 있지만 공유되지 않는다. 수정할 수 없고, 이로 인해 서로의 실행 흐름에 영향을 줄 수 없기 때문이다.
그림 2.5 공유 의존성과 프로세스 외부 의존성 간의 관계.
2.3 고전파와 런던파의 비교
고전파와 런던파 간의 주요 차이는 단위 테스트의 정의에서 격리 문제를 어떻게 다루는지에 있다.
•
책의 저자는 개인적으로 단위 테스트 고전파를 선호한다.
◦
고전파는 고품질의 테스트를 만들고 단위 테스트의 궁극적 목표인 프로젝트의 지속 가능한 성장을 달성하는데 더 적합
◦
이유는 취약성에 있다. 목을 사용하는 테스트는 고전적인 테스트보다 불안정한 경향이 있기 때문이다.
◦
지금은 런던파의 주요 장점을 살펴보자.
▪
입자성이 좋다.
▪
서로 연결된 클래스의 그래프가 커져도 테스트하기 쉽다.
▪
테스트가 실패하면 어떤 기능이 실패했는지 확실히 알 수 있다.
2.3.1 한 번에 한 클래스만 테스트하기
•
런던파는 클래스를 단위로 간주한다. 객체지향 개발자들은 보통 클래스를 모든 코드베이스의 기초에 위치한 원자 빌딩 블록으로 간주한다.
◦
이로 인해 자연스럽게 클래스를 테스트에서 검증할 원자 단위로도 취급
◦
이런 경향은 이해되기는 하지만 오해의 소지가 있다.
▪
테스트는 코드의 단위를 검증해서는 안된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로는 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다.
◦
따라서 좋은 코드의 입자성을 목표로 하는 것은 도움이 되지 않는다.
→ 테스트가 단일 동작 단위를 검증하는 한 좋은 테스트다.
2.3.2 상호 연결된 클래스의 큰 그래프를 단위 테스트하기
•
실제로 목을 사용하면 클래스를 쉽게 테스트 할 수 있다.
◦
테스트 대상 클래스에 의존성이 있고, 이 의존성에 다시 각각의 의존성이 있고, 이렇게 여러 계층에 걸쳐서 계속되는 식으로 의존성 그래프가 복잡하게 있을 때 쉽게 테스트 할 수 있다.
◦
목을 사용하면 클래스의 직접적인 의존성을 대체해 그래프를 나눌 수 있으며, 이는 단위 테스트에서 준비해야 할 작업량을 크게 줄일 수 있다.
•
모두 사실이지만, 위 추리 과정은 잘못된 문제에 초점을 맞추고 있다.
◦
상호 연결된 클래스의 크고 복잡한 그래프를 테스트할 방법을 찾는 대신, 먼저 이러한 클래스 그래프를 갖지 않는 데 집중해야 한다. → 클래스 그래프가 커진 것은 코드 설계 문제의 결과.
2.3.3 버그 위치 정확히 찾아내기
•
런던 스타일 테스트가 있는 시스템에 버그가 생기면 보통 SUT에 버그가 포함된 테스트만 실패한다.
•
고전적인 방식이라면 오작동하는 클래스를 참조하는 클라이언트를 대상으로 하는 테스트도 실패한다.
◦
즉, 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 파급 효과를 초래한다.
◦
문제의 원인을 찾기 어렵고, 디버깅하는 데 시간이 걸릴 수 있다.
•
우려할 만하지만, 큰 문제는 아니다. 테스트를 정기적으로 실행하면 버그의 원인을 알아낼 수 있다.
→ 마지막으로 한 수정이 무엇인지 알수 있기 때문에 문제를 찾는 것은 어렵지 않다.
2.3.4 고전파와 런던파 사이의 다른 차이점
•
고전파와 런던파 사이에 남아있는 두 가지 차이점은 다음과 같다.
◦
테스트 주도 개발을 통한 시스템 설계 방식
◦
과도한 명세 문제
테스트 주도 개발(TDD)
테스트 주도 개발은 테스트에 의존해 프로젝트 개발을 추진하는 소프트웨어 개발 프로세스다.
•
런던 스타일 단위 테스트
◦
하향식 TDD, 전체 시스템에 대한 기대치를 설정하는 상위 레벨 테스트부터 시작
•
고전 스타일 단위 테스트
◦
상향식 TDD, 도메인 모델을 시작으로 최종 사용자가 소프트웨어를 사용할 수 있을 때까지 계층을 그 위에 더 둔다.
•
고전파와 런던파 간의 가장 중요한 차이점은 과도한 명세 문제.
◦
즉 테스트가 SUT의 구현 세부 사항에 결합되는 것이다.
◦
런던 스타일은 고전 스타일보다 테스트가 구현에 더 자주 결합되는 편이다.
▪
이로 인해 런던 스타일과 목을 전반적으로 아무 데나 쓰는 것에 대해 주로 이의가 제기된다.
2.4 두 분파의 통합 테스트
런던파와 고전파는 통합 테스트 정의에도 차이가 있다!!
격리 문제에 대한 견해에서 차이가 나면서 자연스럽게 다른 의견으로 이어졌다.
•
런던파
◦
실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주
◦
고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게 통합 테스트로 느껴진다.
단위 테스트의 세 가지 속성
- 작은 코드 조각을 검증하고
- 빠르게 수행하고
- 격리된 방식으로 처리한다.
이 책에서는 단위 테스트와 통합 테스트의 고전적인 정의를 사용한다. 첫 번째와 세 번째 속성의 의미를 알아봤으니 고전파의 관점에서 다시 정의하면 아래와 같다.
- 단일 동작 단위를 검증하고
- 빠르게 수행하고
- 다른 테스트와 별도로 처리한다.
•
통합 테스트는 위의 고전파 관점의 속성 세 가지 전부를 충족하지 않는 테스트다.
◦
공유 의존성(말하자면 데이터베이스)에 접근하는 테스트는 다른 테스트와 분리해 실행할 수 없다.
▪
데이터베이스 상태 변경이 생기면 병렬로 실행할 때 동일한 데이터베이스에 의존하는 다른 모든 테스트의 결과가 반영될 것이다. 간섭을 피하려면 추가 조치를 취해야 한다.
◦
프로세스 외부 의존성에 접근하면 테스트가 느려진다.
▪
데이터베이스에 대한 호출은 실행 시간에 수백 밀리초를 추가한다. 밀리초가 미미해 보일 수 있지만 테스트 스위트가 커지면 1초가 중요하다.
◦
둘 이상의 동작 단위를 검증할 때의 테스트는 통합 테스트다. 비슷한 단계를 따르지만 다른 동작 단위를 검증하는 느린 테스트가 두 개 있을 때, 하나로 합치는 것이 타당할 수 있다.
▪
비슷한 두 동작을 검증하는 하나의 테스트이지만 세밀한 테스트 두 개보다 빠르게 실행할 수 있다.
통합 테스트는 시스템 전체를 검증해 소프트웨어 품질을 기여하는 데 중요한 역할을 한다.
3부에서 통합 테스트를 자세히 살펴본다.
2.4.1 통합 테스트의 일부인 엔드 투 엔드 테스트
•
통합 테스트는 공유 의존성, 프로세스 외부 의존성뿐 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는지도 검증하는 테스트다.
◦
엔드 투 엔드 테스트는 통합 테스트의 일부로 코드가 프로세스 외부 종속성과 함께 어떻게 작동하는지 검증한다.
◦
통합테스트와의 차이점은 엔드 투 엔드 테스트가 일반적으로 의존성을 더 많이 포함한다는 것이다.
•
일반적으로 통합 테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동한다.
◦
반면에 엔드 투 엔드 테스트는 프로세스 외부 의존성을 전부 또는 대다수 갖고 작동한다.
◦
따라서 엔드 투 엔드라는 명칭은 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것을 의미한다.
•
UI 테스트, GUI 테스트, 기능 테스트와 같은 용어도 사용한다.
◦
용어들이 잘 정의돼 있지 않지만, 일반적으로 모두 동의어
•
애플리케이션이 데이터베이스, 파일 시스템, 결제 게이트웨이라는 세 가지 프로세스 외부 의존성으로 작동한다고 가정하자.
◦
일반적인 통합 테스트는 데이터베이스와 파일 시스템만 포함하고, 결제 게이트웨이는 테스트 대역(목)으로 대체한다.
◦
결제 게이트웨이의 제어 권한은 필요한 상태로 쉽게 가져올 수 없다.
◦
엔드 투 엔드 테스트는 유지 보수 측면에서 비용이 가장 많이 들기 때문에 모든 단위 테스트와 통합 테스트를 통과한 후 빌드 프로세스 후반에 실행하는 것이 좋다.
•
엔드 투 엔드 테스트를 하더라도 모든 프로세스 외부 의존성을 처리하지 못할 수도 있다.
•
따라서 테스트 대역(목)을 사용할 필요가 있고, 통합 테스트와 엔드 투 엔드 테스트 사이에 뚜렷한 경계가 없다는 사실을 강조한다.