카테고리 없음

Unit Testing 5장

eumjo_o 2023. 6. 30. 18:33

5장 목과 테스트 취약성

5.1 목과 스텁 구분

 💡 목 - 데스트 대상 시스템(SUT)과 그 협력자 사이의 상호 작용을 검사할 수 있는 테스트 대역

 

5.1.1 테스트 대역 유형

 💡 테스트 대역 - 테스트를 편리하게 하는 것이 목표인 모든 유형의 비운영용 가짜 의존성을 설명하는 포괄적인 용어

목 (목, 스파이 spy) 스텁 (스텁, 더미 dummy, 페이크 fake)

 

  • 테스트 대역의 모든 변형은 목과 스텁의 두 가지 유형으로 나눌 수 있다.
    • 목 (목, 스파이)
      • 외부로 나가는 상호작용을 모방하고 검사하는데 도움
      • SUT가 상태를 변경하기 위한 의존성을 호출하는 것에 해당
      • SUT와 관련 의존성 간의 상호작용을 모방하고 검사
    • 스텁 (스텁, 더미, 페이크)
      • 내부로 들어오는 상호작용을 모방하는 데 도움
      • SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당
      • SUT가 관련 의존성 간의 상호작용을 모방만 한다.
      • **더미** - null 값이나 가짜 문자열과 같이 단순하고 하드코딩된 값
      • **스텁** - 더 정교, 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 필요한 것을 다 갖춘 완전한 의존성
      • **페이크** - 대다수의 목적에 부합하는 스텁이지만, 보통 아직 존재하지 않는 의존성을 대체하고자 구현

테스트 대상 시스템 ——→ 이메일 발송 (목) ——-→ SMTP 서버

  • 서버에 부작용을 초래하는 상호작용
  • - 외부로 나가는 상호 작용을 모방하는 테스트 대역

테스트 대상 시스템 ——→ 데이터 검색 (스텁) ——-→ 데이터베이스

  • 데이터베이스에서 데이터를 검색하는 것은 부작용을 일으키지 않는 상호작용
  • 스텁 - 내부로 들어오는 상호 작용을 모방하는 테스트 대역

5.1.2 도구로서의 목과 테스트 대역으로서의 목

  • 목 라이브러리의 클래스도 목, 해당 클래스는 실제 목을 만드는 데 도움이 되지만, 그 자체로는 목이 아니다.
    • Mock 클래스 - 도구로서의 목
    • 인스턴스 mock - 테스트 대역으로서의 목
    [Fact]
    public void Sending_a_greetings_email()
    {
    		// Mock(도구)로 mock(목) 생성
        var emailGatewayMock = new Mock<IEmailGateway>();
        var sut = new Controller(emailGatewayMock.Object);
    
        sut.GreetUser("user@email.com");
    
    		// 테스트 대역으로 하는 SUT의 호출을 검사 (외부로 나가는 상호작용)
    		// 목적은 부작용을 일으키는 것 (이메일 발송)
        emailGatewayMock.Verify(
            x => x.SendGreetingsEmail("user@email.com"),
            Times.Once);
    }
    
    [Fact]
    public void Creating_a_report()
    {
    		// Mock(도구)를 사용해 스텁 생성
        var stub = new Mock<IDatabase>();
    		// 준비한 응답 설정 
    		// 테스트 대역은 내부로 들어오는 상호작용, 즉 SUT에 입력 데이터를 제공하는 호출을 모방
        stub.Setup(x => x.GetNumberOfUsers()).Returns(10);
        var sut = new Controller(stub.Object);
    
        Report report = sut.CreateReport();
    
        Assert.Equal(10, report.NumberOfUsers);
    }
    

5.1.3 스텁으로 상호 작용을 검증하지 말라

  • 스텁은 SUT가 출력을 생성하도록 입력을 제공한다
  • 스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티 패턴이다.
  • 테스트에서 거짓 양성을 피하고 리팩터링 내성을 향상시키는 방법은 구현 세부 사항이 아니라 최종결과(이상적으로 비개발자들에게 의미가 있어야 함)를 검증하는 것뿐이다.

5.1.4 목과 스텁 함께쓰기

  • 목과 스텁의 특성을 모두 나타내는 테스트 대역을 만들 필요도 있다.
  • 1, 2 두가지의 서로 다른 메서드를 사용하기 때문에 스텁과의 상호 작용을 검증하지 말라는 규칙은 위배되지 않는다.
  • 테스트 대역은 목이면서 스텁이지만, 목이라고 부르지 스텁이라고 부르지는 않는다.
  • 목이라는 사실이 스텁이라는 사실보다 더 중요하기 때문에 대체로 목이라고 부른다.
[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
    // Arrange
    var storeMock = new Mock<IStore>();
		**// 1. 준비된 응답을 설정**
    storeMock
        .Setup(x => x.**HasEnoughInventory**(Product.Shampoo, 5))
        .Returns(true);
    var customer = new Customer();

    // Act
    bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);

    // Assert
    Assert.True(success);
		**// 2. SUT에서 수행한 호출을 검사** 
    storeMock.Verify(x => x.**RemoveInventory**(Product.Shampoo, 5), Times.Once);
}

5.1.5 목과 스텁은 명령과 조회에 어떻게 관련돼 있는가?

  • 명령 조회 분리(CQS) 원칙에 의하면, 모든 메서드가 명령 또는 조회 중 하나여야 하지만 둘 다는 안된다.
  • 명령을 대체하는 테스트 대역은 목이다.
  • 조회를 대체하는 테스트 대역은 스텁이다.

5.2 식별할 수 있는 동작과 구현 세부 사항

테스트 취약성 - 회귀방지, 리펙터링 내성, 빠른 피드백, 유지 보수성

  • 테스트는 코드가 생성하는 **최종결과(식별할 수 있는 동작)**을 검증해야 한다.
  • 구현 세부사항과 테스트를 가능한한 떨어뜨리는 것이 중요.
  • 테스트는 어떻게가 아니라 무엇에 중점을 둬야 한다.

5.2.1 식별할 수 있는 동작은 공개 API와 다르다

  • 모든 제품 코드는 2차원으로 분류할 수 있다. (둘 다 될 수는 없다)
    • 공개 API or 비공개 API
    • 식별할 수 있는 동작 or 구현 세부 사항
  • 코드의 공개성은 private, public, internal등 접근 제한자에 의헤 제어된다.
  • 다음 요구 사항을 하나라도 충족하면 코드가 시스템의 식별할 수 있는 동작이다.
    • 클라이언트가 목표를 달성하는데 도움이 되는 연산을 노출하라.
      • 연산은 계산을 수행하거나 부작용을 초래하거나 둘 다 하는 메서드
    • 클라이언트가 목표를 달성하는데 도움이 되는 상태를 노출하라.
      • 상태는 시스템의 현재 상태
    클라이언트라는 단어는 코드가 있는 위치에 따라 다른 것을 의미할 수 있다.
    • 동일한 코드베이스, 외부 어플리케이션, 또는 사용자 인터페이스
  • 잘 설계된 API에서 식별할 수 있는 동작공개 API와 일치하는 반면, 모든 구현 세부 사항비공개 API 뒤에 숨어 클라이언트 눈에 보이지 않아야 한다.
  • 공개 API식별할 수 있는 동작의 범위를 넘어서면, 시스템은 구현 세부 사항을 유출한다.

5.2.2 구현 세부 사항 유출: 연산의 예

  • User 클래스 API는 잘 설계되어 있지 않으며, 식별할 수 있는 동작이 아닌 Normalize Name 메서드를 노출하고 있다.
public class User
{
    **public** string Name { get; set; }

    public string NormalizeName(string name)
    {
        string result = (name ?? "").Trim();

        if (result.Length > 50)
            return result.Substring(0, 50);

        return result;
    }
}

public class UserController
{
    public void RenameUser(int userId, string newName)
    {
        User user = GetUserFromDatabase(userId);

        string normalizedName = user.NormalizeName(newName);
        user.Name = normalizedName;

        SaveUserToDatabase(user);
    }
}
  • API가 잘 설계된 User 클래스, 식별할 수 있는 동작만 공개돼 있고, 구현 세부 사항은 이제 비공개
public class User
{
    **private** string _name;
		// 식별할 수 있는 동작 Name 속성만 고액
    public string Name
    {
        get => _name;
				// 클래스의 식별할 수 있는 동작(setter)으로만 테스트에서 확인해야 함. 
        set => _name = NormalizeName(value);
    }
		// 구현 세부사항 NormalizeName 메서드는 비공개 API 뒤에 숨음.
    private string NormalizeName(string name)
    {
        string result = (name ?? "").Trim();

        if (result.Length > 50)
            return result.Substring(0, 50);

        return result;
    }
}

// 애플리케이션 서비스
public class UserController
{
    public void RenameUser(int userId, string newName)
    {
        User user = GetUserFromDatabase(userId);
        user.Name = newName;
        SaveUserToDatabase(user);
    }
}

5.2.3 잘 설계된 API와 캡슐화

<aside> 💡 캡슐화는 코드를 불변성 위반으로부터 보호하는 행위

</aside>

  • 구현 세부 사항을 숨기면 클라이언트의 시야에서 클래스 내부를 가릴 수 있기 때문에 내부를 손상시킬 위험이 적다.
  • 테이터와 연산을 결합하면 해당 연산이 클래스의 불변성을 위반하지 않도록 할 수 있다.
  • 클라이언트는 구현 세부 사항을 사용해 코드의 불변성을 우회할 수 있기 때문에 구현 세부 사항을 노출하면 캡슐화가 위반되는 경우가 종종 있다.

5.2.4 구현 세부 사항 유출:상태의 예

  • 모든 구현 세부 사항을 비공개로 하면 테스트가 식별할 수 있는 동작을 검증하는 것 외에는 다른 선택지가 없으며, 이로 인해 리펙터링 내성도 자동으로 좋아진다.
    • API를 잘 설계하면 단위 테스트도 자동으로 좋아진다.
  • 클라이언트가 목표를 달성하는 데 직접적으로 도움이 되는 코드만 공개해야 하며, 다른 모든 것은 구현 세부 사항이므로 비공개 API 뒤에 숨겨야 한다.
  • 구현 세부 사항(클라이언트가 사용하지 말아야 하는 메서드나 클래스)을 노출할 수 있지만, 식별할 수 있는 동작을 숨길 수는 없다.
  • 코드의 공개 여부와 목적의 관계. 구현 세부 사항을 공개하지 말라식별할 수 있는 동작 구현 세부 사항
    공개 좋음 나쁨
    비공개 해당 없음 좋음

5.3 목과 테스트 취약성 간의 관계

5.3.1 육각형 아키텍쳐 정의

<aside> 💡 육각형 아키텍처는 상호 작용하는 애플리케이션의 집합, 각 애플리케이션은 육각형으로 표시 두 개의 계층 도메인 - 비지니스 로직을 포함 애플리케이션 - 도메인 계층 위에 있음.

</aside>

  • 육각형의 각 계층은 식별할 수 있는 동작을 나타내며, 각각의 구현 세부 사항이 있다.
  • 식별할 수 있는 동작은 바깥 계층에서 안쪽으로 흐른다.
  • 코드 조각이 식별할 수 있는 동작이 되려면 클라이언트가 목표를 달성하도록 도울 필요가 있다.
  • 도메인 클래스의 경우 클라이언트는 애플리케이션 서비스에 해당하고, 애플리케이션 서비스이면 외부 클라이언트에 해당한다.

3가지 관점을 강조

  • 도메인과 애플리케이션 서비스 계층 간의 영향 분리
    • 도메인 계층은 비즈니스 로직을 책임져야 하고, 애플리케이션 서비스 계층의 클래스에 의존해서는 안된다.
    • 도메인 계층을 애플리키에션의 도메인 지식(사용방법) 모음으로, 애플리케이션 서비스 계층을 일련의 비즈니스 유스케이스(사용 대상)으로 볼 수 있다.
  • 애플리케이션 서비스 계층에서 도메인 계층으로의 단방향 의존성 흐름
    • 도메인 계층 내 클래스는 서로에게만 의존해야 하고, 애플리케이션 서비스 계층의 클래스에 의존해서는 안 된다.
    • 애플리케이션 서비스 계층이 도메인 계층에 대해 알고 있지만, 도메인 계층은 외부 환경에서 완전히 격리돼야 한다.
  • 외부 애플리케이션은 애플리케이션 서비스 계층이 유지하는 공통 인터페이스를 통해 연결된다.
    • 아무도 도메인 계층에 직접 액세스할 수 없다.

5.3.2 시스템 내부 통신과 시스템 간 통신

  • 애플리케이션
    • 시스템 내부 통신 - 애플리케이션 내 클래스 간의 통신
    • 시스템 간 통신 - 애플리케이션이 외부 에플리케이션과 통신할 때를 이른다.
  • 시스템 내부 통신은 구현 세부 사항이고, 시스템 간 통신은 전체적으로 애플리케이션의 식별할 수 있는 동작을 나타낸다.

5.3.3 시스템 내부 통신과 시스템 간 통신의 예

  • 시스템 내 통신은 구현 세부 사항
    • 애플리케이션을 통해서만 접근할 수 있는 외부 시스템과의 상호 작용도 구현 세부 사항
      • 그 결과의 부작용을 외부에서 확인할 수 없기 때문
  • 애플리케이션을 통해서만 접근할 수 있는 외부 시스템을 제외하고 시스템 간 통신은 식별할 수 있는 동작

5.4 단위 테스트의 고전파와 런던파 재고

격리 주체 단위의 크기 테스트 대역 사용 대상

런던파 단위 단일 클래스 불변 의존성 외 모든 의존성
고전파 단위 테스트 단일 클래스 또는 클래스 세트 공유 의존성
  • 런던파는 불변 의존성을 제외한 모든 의존성에 목 사용을 권장하며, 시스템 내 통신과 시스템 간 통신을 구분하지 않고, 그 결과 테스트는 애플리케이션과 외부 시스템 간의 통신을 확인하는 것처럼 클래스 간 통신도 확인한다.
  • 런던파 처럼 목을 무분별하게 사용하면 구현 세부 사항에 결합돼 테스트 리펙터링 내성이 없게 됨.
  • 고전파는 테스트 간에 공유하는 의존성(대부분 SMTP 서비스나 메시지 버스 등 프로세스 외부 의존성만 해당)만 교체하자고 하기에 런던파보다 better!
  • 하지만 런던파 만큼은 아니지만, 고전파도 시스템 간 통신 처리가 이상적이진 않고, 목 사용을 지나치게 장려.

5.4.1 모든 프로세스 외부 의존성을 목으로 해야 하는 것은 아니다

  • 프로세스 외부 의존성과의 통신은 외부에서 관찰할 수 없으면 구현 세부 사항이다.
  • 리팩터링 후에 그대로 유지할 필요가 없으므로 목으로 검증해서는 안된다.

5.4.2 목을 사용한 동작 검증

  • 목은 애플리케이션의 경계를 넘나드는 상호 작용을 검증할 떄
  • 이러한 상호 작용의 부작용이 외부 환경에서 보일 때 만 동작과 관련이 있다.