본문 바로가기
Study/단위 테스트

[단위 테스트 스터디] 03. 단위 테스트 구조

by irerin07 2023. 3. 28.
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.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 단위 테스트 명명 지침

  1. 엄격한 명명 정책을 따르지 않는다. 표현의 자유를 허용한다.
  2. 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
  3. 단어를 밑줄(_) 표시로 구분한다.
  • 테스트 클래스 이름을 지을때 [클래스명]Test 패턴을 사용하지만, 테스트가 해당 클래스만 검증 하는 것으로 제한하는건승 아니다.
    • 단위 테스트의 단위는 동작의 단위이지, 클래스의 단위가 아님을 기억하자.

3.4.2 예제: 지침에 따른 테스트 이름 변경

  • 테스트 이름에 SUT의 메서드 이름을 포함하지 말자.
  • 우린 코드를 테스트하는 것이 아닌, 애플리케이션 동작을 테스트하는 것이라는 것을 명심해야한다.
  • SUT는 그저 진입점, 동작을 호출하는 수단이다.
  • 다만 유틸리티 코드를 작업할때는 이 지침은 해당되지 않는다.
    • 비즈니스 로직이랄것이 없고 코드의 동작은 단순 보조 기능이기 때문에 SUT메서드 이름을 사용해도 괜찮다.

3.5 매개변수화된 테스트 리팩터링하기

  • 테스트 하나만으로는 동작 단위를 완전하게 설명하기는 어렵다.
  • 이 동작단위는 일반적으로 여러 구성 요소를 포함하고, 각 구성 요소는 자체 테스트로 캡처해야 한다.
  • 동작이 복잡해지면, 이를 설명하는 데 테스트 수가 급격히 증가할 것이고 이로 인해 관리하기 어려워진다.

매개변수화된 테스트(Parameterized test)를 사용해 유사한 테스트를 묶는 기능

  • 매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만 비용이 발생한다.
    • 테스트 메서드가 나타내는 사실을 파악하기가 어려워졌고 매개변수가 많을수록 더 어렵다.
  • 테스트 코드의 양과 가독성은 서로 상충된다.
  • 저자는 다음과 같이 조언한다
    • 입력 매개변수만으로 테스트 케이스를 판단할 수 있다면 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 하나의 메서드로 둔다.
    • 그게 아니라면 긍정적인 테스트 케이크를 도출한다.
    • 동작이 너무 복잡하다면 매개변수화된 테스트를 사용하지 말고 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 각각 고유의 테스트 메서드로 나타내라.

3.5.1 매개변수화된 테스트를 위한 데이터 생성

3.6 검증문 라이브러리를 사용한 테스트 가독성 향상

728x90