Search

2장 단위 테스트란 무엇인가?

작성자
진행일
2022/09/28
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 테스트, 기능 테스트와 같은 용어도 사용한다.
용어들이 잘 정의돼 있지 않지만, 일반적으로 모두 동의어
애플리케이션이 데이터베이스, 파일 시스템, 결제 게이트웨이라는 세 가지 프로세스 외부 의존성으로 작동한다고 가정하자.
일반적인 통합 테스트는 데이터베이스와 파일 시스템만 포함하고, 결제 게이트웨이는 테스트 대역(목)으로 대체한다.
결제 게이트웨이의 제어 권한은 필요한 상태로 쉽게 가져올 수 없다.
엔드 투 엔드 테스트는 유지 보수 측면에서 비용이 가장 많이 들기 때문에 모든 단위 테스트와 통합 테스트를 통과한 후 빌드 프로세스 후반에 실행하는 것이 좋다.
엔드 투 엔드 테스트를 하더라도 모든 프로세스 외부 의존성을 처리하지 못할 수도 있다.
따라서 테스트 대역(목)을 사용할 필요가 있고, 통합 테스트와 엔드 투 엔드 테스트 사이에 뚜렷한 경계가 없다는 사실을 강조한다.