728x90
단위 테스트 구조
- 단위 테스트 구조
- 좋은 단위 테스트 명명법
- 매개변수화된 테스트 작성
- Fluent Assertions 사용
3.1 단위 테스트를 구성하는 방법
3.1.1 AAA 패턴 사용
- 준비 (Arrange)
- 실행 (Act)
- 검증 (assert)
- 스위트 내 모든 테스트가 단순하고 균일한 구조를 가지도록 도와준다.
- 이는 테스트 스위트의 유지 보수 비용을 감소시킨다.
- 준비
- SUT(테스트 대상 시스템)와 해당 의존성을 원하는 상태로 만든다.
- 실행
- SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력이 있는 경우 출력 값을 캡처한다.
- 검증
- 결과 검증. 반환 값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시한다.
- TDD를 실천하여 기능을 개발하기 전에 실패할 테스트를 만들 때는 아직 기능이 어떻게 동작할지 충분히 모르는 상태이다.
- 따라서 먼저 기대하는 동작으로 윤곽을 잡은 다음, 이러한 기대에 부응하기 위한 시스템을 어떻게 개발할지 아는 것이 좋다.
- 특정 동작이 무엇을 해야 하는지에 대한 목표를 정한 뒤 실제 문제를 해결한다.
3.1.2 여러 개의 준비, 실행, 검증 구절 피하기
준비, 실행 또는 검증 구절이 여러 개 있는 테스트
- 검증 혹은 준비 구절로 구분된 실행구절이 여러개라면 이는 여러 개의 동작 단위를 검증하는 테스트를 뜻한다.
- 이는 단위테스트가 아닌 통합 테스트의 영역이다.
- 각 동작을 고유의 테스트로 도출해야한다.
3.1.3 테스트 내 if 문 피하기
- if 문이 있는 단위 테스트는 안티 패턴이다. 테스트는 분기 없이 간단한 일련의 단계여야 한다.
- if 문은 테스트가 한 번에 너무 많은것을 검증한다는 의미다.
3.1.4 각 구절은 얼마나 커야 하는가?
- 만약 준비 구절이 너무 거대하다면 이를 같은 테스트 클래스 내 비공개 메서드나 별도의 팩토리 클래스도 도출하도록 하자.
- Object Mother, Test Data Builder 참고
- 실행 구절이 두 줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다.
- 단일 작업을 수행하는데 두 개 이상의 메서드의 호출이 필요한 경우를 생각해보자.
- 예제 3.3
- 첫 번째 실행 라인으로 고객이 상점에서 샴푸 다섯개를 구매하고
- 두 번째 실행 라인으로 재고를 감소 시킨다.
- 이는 비즈니스 관점에서 생각하면 구매가 발생과 재고 감소라는 두가지 결과가 만들어 진다.
- 이러한 결과는 같이 만들어져야 하고, 이는 단일 공개 메서드가 있어야 한다는 뜻이다.
- 그렇게 하지 않으면 첫 번째 메서드가 실행되고 두 번째 메서드가 실행이 되지 않으면 모순이 발생한다.
- 이러한 결과는 같이 만들어져야 하고, 이는 단일 공개 메서드가 있어야 한다는 뜻이다.
- 이런 모순을 불변 위반(Invariant Violation)이라 하며 이로부터 코드를 보호하기 위한 행위를 캡슐화(encapsulation)이라 한다.
- Purchase 메서드의 한 부분으로 고객이 구매한 만큼의 재고를 제거하고, 클라이언트 코드에 의존하지 않아야 한다.
- 실행 구절을 한 줄로 하는 지침은 비즈니스 로직을 포함한 대부분의 코드에 적용 되지만 이 역시도 절대적이지는 않다. 유틸리티나 인프라 코드에는 덜 적용된다.
- 예제 3.3
3.1.5 검증 구절에는 검증문이 얼마나 있어야 하는가?
- 가능한 한 가장 작은 코드를 목표로 하는 전제를 기반으로, 테스트당 하나의 검증을 갖는 지침을 들어봤을것이다.
- 이는 올바르지 않다.
- 단위 테스트의 단위는 동작의 단위이고 코드의 단위가 아니다.
- 단일 동작 단위는 여러 결과를 낼 수 있으며 하나의 테스트로 그 모든 결과를 평가하도록 하자
- 디민 검증 구절이 너무 거대해 지는것은 경계해야한다.
- 제품 코드에서 추상화가 누락 되었을 가능성이 있다.
- SUT에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버(Equality member)를 정의하여 단일 검증문으로 객체를 기대값과 비교할 수 있다.
3.1.6 종료 단계는 어떤가
- 별도의 메소드로 테스트에 의해 작성된 파일을 지우거나 DB 연결을 종료하고자 사용할 수 있다.
- 대부분의 단위 테스트는 이가 불필요하다.
- 프로세스 외부에 종속적이면 안되기 때문에 처리해야 할 사이드 이펙트를 남겨선 안된다.
3.1.7 테스트 대상 시스템 구별하기
- SUT는 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공한다.
- 동작은 굉장히 거대할수도 굉장히 작을수도 있지만 진입점은 오직 하나만 존재할 수 있다. (동작을 수행할 하나의 클래스)
- SUT와 의존성을 구분하는것이 중요하다.
- SUT가 많은 경우 테스트 내의 SUT 이름을 쉽게 구분할 수 있도록 sut로 하면 편하다.
3.1.8 준비, 실행, 검증 주석 제거하기
- 테스트 파악을 편하게 하기 위해 각 구절을 서로 구분하는 것 역시 중요하다.
- 다만 대규모 테스트에서는 잘 작동하지 않는다.
- 대규모 테스트에서는 준비 단계에 빈 줄을 추가해 설정 단계를 구분한다.
- 통합 테스트에는 복잡한 설정을 포함하는 경우가 있으므로
- AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석을 제거한다.
3.2 xUnit 테스트 프레임워크 살펴보기
- 각 테스트는 이야기가 있어야 한다
- 테스트가 통과하는건 이야기(시나리오)가 사실 이라는 증거이고 실패한다면 해당 이야기는 더 이상 유효하지 않다는 뜻이다.
- 테스트 재작성 혹은 시스템 자체를 수정해야한다.
- 테스트는 단순히 제품 코드의 기능을 나열하는것이 아니다. 애플리케이션 동작에 대해 고수준의 명세가 있어야 한다.
- 이 명세는 개발자뿐만이 아니라 비즈니스 담당자에게도 의미가 있어야 한다.
- 테스트가 통과하는건 이야기(시나리오)가 사실 이라는 증거이고 실패한다면 해당 이야기는 더 이상 유효하지 않다는 뜻이다.
3.3 테스트 간 테스트 픽스처 재사용
- 테스트 픽스처
- 테스트 실행 대상 객체이며 SUT로 전달되는 인수다. DB에 있는 데이터나 디스크의 파일일 수도 있다. 이런 객체는 테스트 실행 전에 알려진 고정 상태로 유지하지 때문에 항상 동일한 결과를 생성한다.
- 준비 구절에서 코드를 재사용하는 것이 테스트를 줄이고 단순화 하기 좋다.
- 테스트 픽스처의 준비는 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다
- 테스트 생성자에서 픽스처를 초기화 하는 것은 바람직하지 못하다.
3.3.1 테스트 간의 높은 결합도는 안티 패턴이다
- 테스트 생성자에서 픽스처를 초기화 하는 것은 테스트 간 결합도를 높인다.
- 테스트의 준비 로직을 수정하면 클래스의 모든 테스트에 영향을 미친다.
- 테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다.
- 이 지침을 따르기 위해서는 테스트 클래스에 공유 상태를 두지 말아야 한다.
3.3.2 테스트 가독성을 떨어뜨리는 생성자 사용
- 준비 코드를 생성자로 추출하면 테스트의 가독성을 떨어뜨릴수 있다.
- 테스트만 보고는 무엇을 하는지 이해하기가 어려워지도 이를 위해 클래스의 다른 부분도 봐야 한다.
- 준비 로직이 별로 없더라도 테스트 메서드로 바로 옮기는 것이 좋다.
3.3.3 더 나은 테스트 픽스처 재사용법
- 테스트 클래스에 비공개 팩토리 메서드를 둔다.
- 공통 초기화 코드를 비공개 팩토리 메서드로 추출, 테스트 코드를 짧게 만들고, 동시에 테스트 진행 상황에 대한 전체 맥락을 유지 할 수 있다.
- 비공개 메서드를 충분히 일반화하는 한 테스트가 서로 결합되지 않는다.
- 다만 테스트 픽스처 재사용 규칙에 한가지 예외가 있는데, 데이터베이스와 작동하는 통합 테스트에 종종 해당한다.
- 이런 테스트는 DB 연결이 필수이며 이 연결을 한 번 초기화한 다음 어디서나 사용할 수 있다. 하지만 이보단 기초 클래스를 둬서 개별 테스트 클래스가 아니라 클래스 생성자에서 데이터베이스 연결을 초기화 하는것이 더 합리적이다.
3.4 단위 테스트 명명법
- 테스트에 표현력 있는 이름을 붙이는것은 중요하다.
- 테스트가 검증하고자 하는 내용과 기본 시스템의 동작을 이해하는데 도움이 된다.
- [테스트 대상 메서드][시나리오][예상결과]
- 가장 유명하고 가장 도움안되는 명명법
- 구현 세부사항에 집중하게 만들어서 별로 좋지 못하다.
- 다른 사람들(개발자 포함)이 보고 파악하기 쉬워야 한다.
3.4.1 단위 테스트 명명 지침
- 엄격한 명명 정책을 따르지 않는다. 표현의 자유를 허용한다.
- 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
- 단어를 밑줄(_) 표시로 구분한다.
- 테스트 클래스 이름을 지을때 [클래스명]Test 패턴을 사용하지만, 테스트가 해당 클래스만 검증 하는 것으로 제한하는건승 아니다.
- 단위 테스트의 단위는 동작의 단위이지, 클래스의 단위가 아님을 기억하자.
3.4.2 예제: 지침에 따른 테스트 이름 변경
- 테스트 이름에 SUT의 메서드 이름을 포함하지 말자.
- 우린 코드를 테스트하는 것이 아닌, 애플리케이션 동작을 테스트하는 것이라는 것을 명심해야한다.
- SUT는 그저 진입점, 동작을 호출하는 수단이다.
- 다만 유틸리티 코드를 작업할때는 이 지침은 해당되지 않는다.
- 비즈니스 로직이랄것이 없고 코드의 동작은 단순 보조 기능이기 때문에 SUT메서드 이름을 사용해도 괜찮다.
3.5 매개변수화된 테스트 리팩터링하기
- 테스트 하나만으로는 동작 단위를 완전하게 설명하기는 어렵다.
- 이 동작단위는 일반적으로 여러 구성 요소를 포함하고, 각 구성 요소는 자체 테스트로 캡처해야 한다.
- 동작이 복잡해지면, 이를 설명하는 데 테스트 수가 급격히 증가할 것이고 이로 인해 관리하기 어려워진다.
매개변수화된 테스트(Parameterized test)를 사용해 유사한 테스트를 묶는 기능
- 매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만 비용이 발생한다.
- 테스트 메서드가 나타내는 사실을 파악하기가 어려워졌고 매개변수가 많을수록 더 어렵다.
- 테스트 코드의 양과 가독성은 서로 상충된다.
- 저자는 다음과 같이 조언한다
- 입력 매개변수만으로 테스트 케이스를 판단할 수 있다면 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 하나의 메서드로 둔다.
- 그게 아니라면 긍정적인 테스트 케이크를 도출한다.
- 동작이 너무 복잡하다면 매개변수화된 테스트를 사용하지 말고 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 각각 고유의 테스트 메서드로 나타내라.
3.5.1 매개변수화된 테스트를 위한 데이터 생성
- Junit5 @Parameterized Test
- https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
3.6 검증문 라이브러리를 사용한 테스트 가독성 향상
- 책에서 소개하는 Fluent Assertions와 비슷한 AssertJ, Hamcrest
- https://dzone.com/articles/hamcrest-vs-assertj-assertion-frameworks-which-one
728x90
'Study > 단위 테스트' 카테고리의 다른 글
[단위 테스트 스터디] 02. 단위 테스트란 무엇인가 (0) | 2023.03.26 |
---|