yepdi / TIL

Today I Learn
0 stars 0 forks source link

Unit Testing 단위테스트 #22

Open yepdi opened 2 years ago

yepdi commented 2 years ago

단위 테스트 책 에서 정리 할 내용 기록

yepdi commented 2 years ago

1장 단위 테스트의 목표

커버리지 비용

성공적인 테스트 스위트

yepdi commented 2 years ago

2장 단위 테스트란?

격리 문제

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

  격리 주체 단위의 크기 테스트 대역 사용 대상
런던파 단위 단일 클래스 불변 의존성 외 모든 의존성
고전파 단위 테스트 단일 클래스 또는 클래스 세트 공유 의존성

통합 테스트

엔드 투 엔드 테스트

yepdi commented 2 years ago

3장 단위 테스트 구조

단위 테스트를 구성하는 방법

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

public class CustomerTests {

    public void Purchase_suceeds_when_enough_inventory() {
          Store store = CreateStoreWithInventory(Product.Shampoo, 10)
          ....
    }

    public Store CreateStoreWithInventory(Product product, int quantity) { <- 비공개 팩토리 메서드 
   ....
    }
}

단위 테스트 명명법

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

참고 자료 : https://www.baeldung.com/parameterized-tests-junit-5

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

yepdi commented 2 years ago

4장 좋은 단위 테스트의 4대 요소

회귀방지

리팩터링 내성

테스트 정확도 극대화

오류 유형 표 작동 고장
테스트결과 테스트 통과 올바른 추론 2종 오류 (거짓 음성)
테스트 실패 1종 오류 (거짓 양성) 올바른 추론

빠른 피드백과 유지보수성

이상적인 테스트

테스트 피라미드

블랙박스 테스트, 화이트박스 테스트

  회귀 방지 리팩터링 내성
화이트박스 테스트 좋음 나쁨
블랙박스 테스트 나쁨 좋음
yepdi commented 2 years ago

5장 목과 테스트 취약성

목과 스텁 구분

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

  식별할 수 있는 동작 구현 세부사항
공개 좋음 나쁨
비공개 해당 없음 좋음

육각형 아키텍처

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

p. 173 예제 코드

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

yepdi commented 2 years ago

6장 단위 테스트 스타일

단위 테스트의 세 스타일

고전파는 상태 기반 스타일 선호. 런던파는 통신 기반 스타일 선호

단위 테스트 스타일 비교

  출력 기반 상태 기반 통신 기반
리팩터링 내성을 위한 노력 낮음 중간 중간
유지비 낮음 중간 높음

함수형 아키텍처

함수형 아키텍처와 육각형 아키텍처

함수형 아키텍처 설계시 고려해야할 점

=> 출력 기반 스타일과 상태 기반 스타일 조합하며 통신 기반 스타일을 섞어도 된다

yepdi commented 2 years ago

7장 가치 있는 단위 테스트를 위한 리팩터링

리팩터링할 코드 식별하기

복잡도 및 도메인 유의성 도메인 모델 및 알고리즘 지나치게 복잡한 코드
-- 간단한 코드 컨트롤러
  협력자 수

험블 객체 패턴

고객 관리 시스템

int newNumberOfEmployees = user.changeEmail(newEmail, companyDomainName, numberOfEmployees)

- user에서 업데이트 된 직원 수 반환 하는 부분 => **책임을 잘못 둔 것 + 추상화가 없다**
  - 새로운 도메인 클래스인 **Company**를 만들자

```java
public class Company {
  private String DomainName;
  private int numberOfEmployees;

  public void changeNumberOfEmployees(int delta) {
    numberOfEmployees += delta;
  }

  public Boolean IsEmailCoporate(String email) {
    String emailDomain = email.split("@")[1];
    return emailDomain == domainName;
  }
}

최적의 단위 테스트 커버리지 분석

컨트롤러에서 조건부 로직 처리

의사 결정 프로세스의 중간 결과를 기반으로 프로세스 외부 의존성에서 추가 데이터를 조회할 경우에는...

public String canChangeEmail() {
   if (IsEmailConfirmed) return "cannot change";
   return null;
}

public void changeEmail(String newEmail, Company company) {
  if (canChangeEmail() == null) return;
  ...
}
// 도메인 모델
public void changeEmail(String newEmail, Company company) {
  ....
  // 새 이벤트 추가 
  emailChangedEvents.add(new EmailChangedEvent(userId, newEmail));
}

// 컨트롤러 
public void changeEmail(int userId, String new email) {
  for (EmailChangedEvent event : user.emailChangedEvents) {
    _messageBus.sendEmailChangedMessage(event.userId, event.newEmail);
  }
}

결론

yepdi commented 2 years ago

8장 통합 테스트를 하는 이유

어떤 프로세스 외부 의존성 테스트?

=> 관리 의존성은 실제 인스턴스를 사용하고 비관리 의존성은 목으로 대체하라

jpa 를 사용하는 경우에는 interface가 같으니까 목으로 대체해도 리팩터링 저하가 일어나지 않을 것 같음 😓

통합 테스트 모범 사례

로깅 기능 테스트

yepdi commented 2 years ago

9장 목 처리에 대한 모범 사례

목의 가치를 극대화하기

public void Changing_email_from_corporate_to_non_corporate() {
   Mock messageBusMock = new Mock<IMessageBus>();
   Mock loggerMock = new Mock<IDomainLogger>();

   messageBusMock.Verify(x => x.SendEmailChangeMesage(user.UserId, "new@gmail.com"), Times.Once);
   loggerMock.Verify(x => x.UserTypeHasChanged(user.UserId, UserType.Employee, UserType.Customer), Times.Once);
}

비관리 의존성인 IMessageBus와 IDomainLogger를 목으로 처리했다.

시스템 끝에서 상호 작용 검증하기

public interface IMessageBus

public class MessageBus : IMessageBus {
    privae IBus _bus;

목을 스파이로 대체하기

목은 프레임워크의 도움을 받아 생성. 스파이는 수동으로 작성

public interface IBus {
   void send(String message);
}

public class BusSpy implement IBus {
   private List<String> sentMsg = new ArrayList<String>();

  public void send(String message) {
     sentMessage.add(message);
  }

  // 검증 로직
  public BusSpy ShouldSendNumberOfMessages(int number) {
     Assertions.assertThat(number).isEqualTo(sentMsg.size());
  }
  public BusSpy withEmailChangedMessage(int userId, String newEmail) {
    String message = "Type" + userId + "Email" + newEmail;
    Assertions.Contains(sentMsg, x -> x == message);
  }
  }
}

목 처리 모범 사례

yepdi commented 2 years ago

11장 단위 테스트 안티 패턴

비공개 메서드 단위 테스트

  식별할 수 있는 동작 구현 세부 사항
공개 좋음 나쁨
비공개 해당 없음 좋음

식별할 수 있는 동작을 공개로 하고 구현 세부 사항을 배공개로 하면 API 가 잘 설계된 것. 식별할 수 있는 동작과 비공개 메서드가 만나는 부분은 해당 없음으로 되어 있다.

public class Inquiry {
    private Inquiry(bool isApproved, DateTime timeApproved) {
     ...
    }
    public void approve(DateTime now) {
    ....
    }
}

ORM 라이브러리에 의해 데이터베이스에서 클래스가 복원되기 때문에 비공개 생성자는 비공개이다. 객체를 인스턴스화 할 수 없다. Inquiry 생성자는 비공개면서 식별할 수 있는 동작인 메서드의 예이다. 이 생성자는 ORM과의 계약을 지키며, 생성자가 비공개라고 해서 계약이 덜 중요하지 않다

비공개 상태 노출

class Customer {
    private CustomerStatus status = CustomerStatus.Regular;
    public void promote() {
       status = CustomerStatus.Preferred;
    }
    public float getDiscount() {
        return status == CustomerStatus.Preferred? 0.05 : 0
    }
}

promote 메서드를 테스트 하려면 필드가 비공개 이므로 테스트 할 수 가 없다. 이 때는 새로 생성된 고객은 할인이 없음. 업그레이드시 5% 할인율 적용 같이 제품 코드가 클래스를 어떻게 사용되는지를 분석한다

테스트로 유출된 도메인 지식

public class CalculatorTest {
    @Test
    public void add_two_number {
         int expected = value1 + value2; // Calculator.Add의 알고리즘과 동일한 로직 
         int actual = Calculator.Add(value1, value2);
    }
}

테스트는 제품 코드에서 알고리즘 구현을 복사했다. 단순히 제품 코드에서 복사/붙여넣기를 한 것. 이것은 구현 세부사항과 결합되는 또다른 예이다.

코드 오염

public class Logger {
    private Boolean isTestEnv;
    public Logger(Boolean isTestEnv) {
       this.isTestEnv = isTestEnv;
    }
    public void log(String text) {
        if (isTestEnv) return;
        ....
    }
}

if문을 통해서 운영환경인지 테스트 환경인지를 구별해 운영 환경이라면 메시지를 파일에 기록하도록 되어 있다. 테스트 코드를 제품 코드 베이스와 분리해야 한다

public interface ILogger {
    void log(String text);
}

public class FakeLogger: ILogger { // 테스트용
}

public class Logger: ILogger { 
}

구체 클래스를 목으로 처리하기

구체 클래스를 대신 목으로 처리할 수 도 있으나 단일 책임 원칙을 위배한다 클래스 내 비관리 외부 의존성을 호출하는 경우, 비 관리 의존성을 스텁으로 교체해야 한다.

즉, 클래스 내 도메인 로직과 비관리 의존성이 결합되어 있는 경우이다

=> 클래스를 두 부분으로 나눈다. 비관리 의존성과 도메인 로직을 분리하여 클래스를 나눈다 p.386

시간 처리하기

시간에 따라 달라지는 기능을 테스트하는 경우 거짓 양성이 발생할 수 잇다.

public interface IDateTimeServer {
    public getTime();
}
public class DateTimeServer: IDateTimeServer {
   public getTime() {
    return DateTime.Now;
   }
}
public class InquiryController {
   IDateTimeServer datetime;
    public InquiryController(IDateTimeServer dateTime) { // 시간을 서비스로 주입
    }
    public void approveInquiry(int id) {
        inquiry.approve(datetime.getTime()).  // 시간을 일반 값으로 주입 
    }
}