phantasmicmeans 기술 블로그

유닛테스트 3장- 단위 테스트 구조 본문

Tech

유닛테스트 3장- 단위 테스트 구조

phantasmicmeans 2023. 7. 10. 09:58

3.1 단위 테스트 구성 방법

AAA 패턴 사용

우리가 흔히 사용하는 give/when/then 과 동일하고, 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다.

  • 준비 구절: SUT 와 해당 의존성을 원하는 상태로 만듬
  • 실행 구절: SUT 메소드 호출 및 준비된 의존성 전달
  • 검증 구절: 결과 검증 (반환 값이나 협력자의 최종 상태, SUT 가 협력자에 호출한 메소드 등으로 표시)

여러개의 준비, 실행, 검증 구절을 피하라

  • 여러 개의 준비, 실행, 검증 구절은 테스트가 너무 많은 것을 한 번에 검증한다는 의미이다.
  • 각 동작을 고유의 테스트로 나눠라
  • 통합테스트의 범주 - 통테에서는 속도를 높이기 위해 이렇게 해도 괜찮다.

테스트 내 if 문 피해라

  • if 문은 테스트가 한 번에 너무 많은 것을 표시

각 구절 사이즈

  • 준비 구절

    • 일반적으로 가장 크다. 코드 재사용해라 (오브젝트 마더, 테스트 데이터 빌더 패턴)
  • 실행 구절

    • 실행 구절은 보통 코드 한 줄, 2줄 이상인 경우 테스트 대상인 public API의 설계 문제가 아닐지 확인해라.
    • 캡슐화를 지켜서, 불변 위반을 제거하고 -> 실행 구절은 한줄로 하라 (유틸성 빼고)
  • 검증 구절

    • 단위테스트 단위는 동작 - 단일 동작 단위는 여러 결과를 낼 수 있으니, 하나의 테스트로 모든 결과를 평가하는게 좋다. (잘 이해가 안감)
    • 그렇다고 하더라도 검증 구절이 너무 커지는건 경계하라 -> 동등 멤버 (equals 같은)를 정의하여 줄여라.
  • 종료 구절

    • 단위테스트에는 따로 필요 없어야한다. 프로세스 외부 종속적이지 않아야 사이드 이펙트가 없다.
    • 통합테스트의 영역이다.

테스트 대상 시스템 구별

  • 테스트 내에서의 SUT의 변수명을 sut로 하라

준비, 실행, 검증 주석

  • 주석을 달거나, 빈 줄로 분리하라 > 빈 줄은 대부분의 단위 테스트에서 간결성, 가독성 등 균형있다.
  • 테스트가 커지면 뭐 마음대로 하라... 구절 주석을 유지하든지.. 제거하든지..

3.2 xUnit

  • 자랑 time

3.3 테스트 간 테스트 픽스처 재사용

테스트 픽스처

  • 테스트의 실행 대상 객체
  • 정규 의존성, 즉 SUT로 전달되는 인수다. (ex. repository 등)

테스트에서 언제 어떻게 코드를 재사용하는지 아는 것이 중요하다. 준비 구절에서 코드 재사용은 테스트를 단순화하기 좋은 방법.

  • 테스트 픽스처 재사용 방법은 2개

A. 생성자(setup)류 - 테스트 픽스처 재사용 방법

  • 테스트 생성자 혹은 init과 같은 setup 특성 메소드에서 픽스처 초기화
  • 준비 구절 코드는 대부분 제거할 수 있으나 단점 존재
    • 테스트 간 결합도가 높아짐
    • 테스트 가독성이 떨어짐
using Book.Chapter2.Listing1;
using Xunit;

namespace Book.Chapter3.CustomerTests_3
{
    public class CustomerTests
    {
        private readonly Store _store;
        private readonly Customer _sut;

        public CustomerTests()
        // 클래스 내 각 테스트 이전에 호출 
        {
            _store = new Store();
            _store.AddInventory(Product.Shampoo, 10);
            _sut = new Customer();
        }

        [Fact]
        public void Purchase_succeeds_when_enough_inventory()
        {
            bool success = _sut.Purchase(_store, Product.Shampoo, 5);

            Assert.True(success);
            Assert.Equal(5, _store.GetInventory(Product.Shampoo));
        }

        [Fact]
        public void Purchase_fails_when_not_enough_inventory()
        {
            bool success = _sut.Purchase(_store, Product.Shampoo, 15);

            Assert.False(success);
            Assert.Equal(10, _store.GetInventory(Product.Shampoo));
        }
    }
}

테스트 간의 높은 결합도는 안티 패턴이다

  • 위 A 예시에서는 테스트 아래 처럼 준비 로직 수정시 모든 테스트에 영향을 미친다.
  • _store.AddInventory(Product.Shampoo, 10); -> _store.AddInventory(Product.Shampoo, 15);
  • 테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다.

그러므로 테스트 클래스에 공유 상태를 두지 말아야한다.

  • 아래 필드들이 공유 상태 예시
private readonly Store _store;
private readonly Customer _sut;

테스트 가독성을 떨어뜨리는 생성자

  • 생성자로 준비코드 추출시 테스트 가독성을 떨어트린다. 테스트만 보고 전체 그림을 볼 수 없다.
  • 준비코드가 별로 없더라도 테스트 메소드로 바로 옮기는게 좋다.
  • 독립적인 테스트는 애매모호한 불확실성을 두지 않는다. (헷갈리게 하지 않는다.)

B. 비공개 팩토리 메소드 - 테스트 픽스처 재사용 방법

테스트 클래스에 비공개 팩토리 메소드를 두어라

  • 공통 초기화 코드를 비공개 팩토리 메소드로 추출하여, 테스트 전체 맥락을 유지하라
  • 아래처럼 읽기 쉽고 재사용 가능하게 테스트 픽스처를 생성하라
    • Store store = CreateStoreWithInventory(Product.Shampoo, 10)
  • 이렇게 비공개 메소드를 간편하게 일반화 하는것도 쉽지 않은 것 같은데.. 어느정도는 자유롭게 풀어 쓰는것도 괜찮다고 생각
    • 테스트 코드도 코드긴하나, 현실적으로 하나하나 챙기면서 가면 지칠거같다.
    • 비공개 메소드도 생성자랑 동일하게 변경 될 수 있다..

생성자든 뭐든 결국 이 글이 말하고자 함은 공유 상태의 컨트롤(공유 상태를 테스트 내에서 공유하지 말자) 측면으로 이해된다.


namespace Book.Chapter3.CustomerTests_4
{
    public class CustomerTests
    {
        [Fact]
        public void Purchase_succeeds_when_enough_inventory()
        {
            Store store = CreateStoreWithInventory(Product.Shampoo, 10);
            Customer sut = CreateCustomer();

            bool success = sut.Purchase(store, Product.Shampoo, 5);

            Assert.True(success);
            Assert.Equal(5, store.GetInventory(Product.Shampoo));
        }

        private Store CreateStoreWithInventory(Product product, int quantity)
        {
            Store store = new Store();
            store.AddInventory(product, quantity);
            return store;
        }
    }
}

3.4 단위 테스트 명명법

  • [테스트 대상 메서드][시나리오][예상결과]
  • 동작 대신 구현 세부 사항에 집중하게끔 부추기기에 도움이 되지 않는다.

초반에 정확히 이런식으로 많이 작성했었다. 지금 와서 보면 뭐하는 테스트인지 알수가 없음 ㅋㅋ..

  • 그래도 케바케로 가끔 예상 결과를 디테일하게 적어놓는것은 도움이 되더라.
  • 특정 에러가 떨어지는것을 보장해야 할 경우 - onErrorResume 등 특정 에러만을 복구하게끔 코드 구성

다음 지침을 따르자.

  • 엄격한 명명 정책을 따르지 말자. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자 안에 넣을 수 없다. 표현의 자유 허용
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자. 도메인 전문가나 비즈니스 분석가가 좋은 예다.
  • 단어를 밑줄 표시로 구분한다.

테스트 클래스 이름

  • [클래스명]Tests 패턴
  • 단위 테스트의 단위는 동작임을 잊지 말고..
  • 그래도 어딘가에선 시작해야하니, [클래스명]Tests를 동작 단위로 검증할 수 있는 진입점으로 보자

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

  • 잘못된 날짜의 배송을 올바르게 식별하는지 검증하는 테스트
  • isDeliveryValid_InvalidDate_ReturnsFalse()
  • Delivery_with_invalid_date_should_be_considered_invalid()
  • ...
  • 수정 결과: Delivery_with_a_past_date_is_invalid()

실제 예시

3.5 매개변수화된 테스트 리팩토링

  • 보통 테스트 하나로 동작 단위를 완전하게 설명하기 충분지 않다.
  • 동작이 충분히 복잡하면 이를 설명하는데 테스트 수가 급격히 증가, 관리가 어려워질 수 있다.

Test 대상: 배송일이 오늘로부터 이틀 후가 되도록 작동하는 배송 기능

  • 지난 날짜: Delivery_with_a_past_date_is_invalid()
  • 오늘 날짜: Delivery_with_for_today_is_invalid()
  • 내일 날짜: Delivery_with_for_tomorrow_is_invalid()
  • 이틀 후 날짜 : The_soonest_delivery_date_is_two_days_from_now()
  • else: ...

유일한 차이는 배송 날짜뿐이나 테스트가 여러가지 나옴 -> 이러한 테스트를 하나로 묶자

  • junit의 @ParameterizedTest
  • 예제 코드처럼 작성시 테스트 메서드가 나타내는 사실을 파악하기 어려움

절충안

  • 긍정적인 테스트 케이스만 고유한 테스트로 도출한다.
  • 코드 28~59 line

그러나, 동작이 너무 복잡하면 매개변수화 테스트를 조금도 사용하지 말아라, 긍정/부정 모두 각각 고유의 메소드로 나타내라.

3.6 검증문 라이브러리를 통해 가독성 향상 (Feat.Fluent Assertions)

namespace Book.Chapter3.FluentAssertions_1
{
    public class CalculatorTests
    {
        [Fact]
        public void Sum_of_two_numbers()
        {
            double first = 10;
            double second = 20;
            var sut = new Calculator();

            double result = sut.Sum(first, second);

            result.Should().Be(30);
        }
    }

    public class Calculator
    {
        public double Sum(double first, double second)
        {
            return first + second;
        }
    }
}

'Tech' 카테고리의 다른 글

BFS로 서비스 스펙 나름 재밌게 친 썰  (0) 2022.07.05
약 500건의 unit test를 작성하고 느낀점  (0) 2022.07.05
MongoDB CDC, Change Streams  (0) 2021.11.12
Neo4j memory 구성  (0) 2020.07.01
Kafka  (0) 2020.06.03
Comments