티스토리 뷰

책도 읽고

단위테스트 8-11장

eungding 2023. 4. 14. 17:42
반응형

단위 테스트 8장~10장 (Part 3. 통합 테스트)  /  11장 (Part 4. 단위테스트 안티패턴)  내용을 기반으로 하고 있습니다.


 

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

 

# 통합테스트의 역할

 

 

통합테스트는 시스템이 프로세스 외부 의존성과 통합해 작동하는 방식을 검증한다. 

 

단위테스트 - 도메인 모델 및 알고리즘 확인

통합테스트 - 컨트롤러 확인  // 외부의존성과 도메인 모델을 연결하는 코드를 확인

 

 

다시 한번 강조하지만, 모든 테스트는 도메인 모델과 컨트롤러 사분면에만 초점을 맞춰야한다. 

 

 

단위테스트가 아닌 모든 테스트가 통합테스트에 해당한다.

(단위테스트 = 단일 동작 단위를 검증하고, 빠르게 수행하고, 다른 테스트와 별도로 처리한다)

 

 

# 다시 보는 테스트 피라미드 

 

단위 테스트 - 유지보수성, 피드백 속도가 우수

통합 테스트 - 회귀방지, 리팩터링 내성이 우수 

 

단위테스트를 통해 가능한 많은 비즈니스 시나리오의 예외 상황을 확인하라.

통합테스트를 사용해서 하나의 주요 흐름 (happy path)과 단위테스트로 확인할 수 없는 예외사항(edge case)을 다루도록 하라.

 

대부분을 단위 테스트로 전환하면 유지비를 절감할 수 있다. 

또한 중요한 테스트가 비즈니스 시나리오당 하나 또는 두개 있으면 시스템 전체의 정확도를 보장할 수 있다. 

 

 

# 어떤 프로세스 외부 의존성을 직접 테스트해야하는가?

 

통합테스트로 외부의존성과의 통합을 검증하는 방식은 두가지가 있다.

 

1. 실제 프로세스 외부 의존성 사용

2. 해당 의존성을 목으로 대체 

 

두가지 방식을 각각 언제 적용해야할까? 

 

5장에서 모든 프로세스 외부 의존성은 두가지 범주로 나눈다고 했다.

 

1. 관리 의존성  // 외부에서 관찰 X,  실제 인스턴스 사용하기

애플리케이션을 통해서만 접근가능. 해당 의존성과의 상호작용은 외부환경에서 볼 수 없음. 대표적인 예로 애플리케이션 데이터베이스.

 

2. 비관리 의존성   // 외부에서 관찰 O,  목으로 대체하기

해당의존성과의 상호작용을 외부에서 볼 수 있음. 예를들어 SMTP 서버, 메세지 버스. 

 

관리의존성은 구현 세부사항, 

비관리의존성은 시스템의 식별할 수 있는 동작이다.

 

그러므로  관리의존성은 실제 인스턴스를 사용하고 비관리의존성은 목으로 대체하라. 

 

통합테스트에서 관리 의존성의 실제 인스턴스를 사용하면 외부 클라이언트 관점에서 최종 상태를 확인할 수 있다.

또한 컬럼 이름을 변경하거나 데이터베이스를 이관하는 등 데이터베이스 리팩터링에도 도움이 된다. 

 

 

# 어떤 시나리오를 테스트할까?

 

통합테스트의 일반적인 지침은 가장 긴 주요흐름과 단위테스트로는 수행할 수 없는 모든 예외상황을 다루는 것이다. 

가장 긴 주요 흐름은 모든 프로세스의 외부 의존성을 거치는 것이다. 

 

 

# 의존성 추상화를 위한 인터페이스 사용

 

많은 개발자가 데이터베이스, 메세지 버스 같은 프로세스 외부 의존성을 위한 인터페이스를 도입한다.

심지어 인터페이스에 구현이 하나만 있는 경우에도 그렇다. 이 관습은 오늘날 널리 퍼져 있어서 아무도 의문을 제기하지 않는다.

다음과 비슷한 클래스, 인터페이스 쌍을 자주 볼 수 있다.

public interface IMessageBus
public class MessageBus: IMessageBus

public interface IUserRepository
public class UserRepository: IUserRepository

 

단일 구현을 위한 인터페이스는 추상화가 아니며, 해당 인터페이스를 구현하는 구체 클래스보다 결합도가 낮지 않다.

진정한 추상화는 발견하는 것이지, 발명하는 것이 아니다.

 

인터페이스가 진정으로 추상화되려면 구현이 적어도 두 가지는 있어야한다.

 

OCP (open-closed prinicple) 를 지킨다고 반박할 수 도 있지만,,

더 기본적인 원칙인  YAGNI("You aren't gonna need it"의 약자. 현재 필요하지 않은 기능에 시간을 들이지말라) 를 위반하기 때문에 잘못된 생각이다.

 

이런 의존성을 목으로 처리할 필요가 없는 한,  인터페이스를 두지 말라.

비관리 의존성만 목으로 처리하므로, 결국 비관리 의존성에 대해서만 인터페이스를 쓰라는 지침이 된다. 

관리 의존성을 컨트롤러에 명시적으로 주입하고, 해당 의존성을 구체클래스로 사용하라. 

 

 

# 테스트에서 다중 실행 구절 사용

 

다중 실행 구절을 사용하는 테스트 의 예다.

더보기

- 준비: 사용자 등록에 필요한 데이터 준비
- 실행: UserController.RegisterUser() 호출
- 검증: 등록이 성공적으로 완료됏는지 확인하기 위해 데이터베이스 조회
- 실행: UserController.DeleteUser() 호출
- 검증: 사용자가 삭제됐는지 확인하기 위해 데이터베이스 조회

 

사용자의 상태가 자연스럽게 흐르기 때문에 설득력이 있고

첫번째 실행이 두번째 실행의 준비 단계 역할을 할 수 있다.

 

문제는 이러한 테스트가 초점을 잃고 순식간에 너무 커질 수 있다는 것이다.

 

각 실행을 고유의 테스트로 추출해 테스트를 나누는 것이 좋다. 불필요한 작업처럼 보이지만 이 작업은 장기적으로 유리하다. 각테스트가 단일 동작 단위에 초점을 맞추게 하면, 테스트를 더 쉽게 이해하고 필요할 때 수정할 수 있다.

 

이 지침의 예외로, 원하는 상태로 만들기 어려운 프로세스 외부 의존성으로 작동하는 테스트가 있다.

예를들어, 은행에서 샌드박스를 제공하는데 샌드박스가 너무 느리거나 은행에서 해당 샌드박스에 대한 호출 수를 제한하는 경우다.

이런 시나리오에서는 여러 동작을 하나의 테스토로 묶어서 문제가 있는 프로세스 외부 의존성에 대한 상호작용 횟수를 줄이는 것이 유리하다. 

 

둘 이상의 실행 구절로 테스트를 작성하는 것이 타당한 이유를 생각해보면, 프로세스 외부 의존성을 관리하기 어려운 경우 뿐이다.

(따라서 단위테스트는 프로세스 외부 의존성으로 작동하지 않기 때문에 절대로 실행구절이 여러개 있어서는 안된다.

통합테스트조차도 실행을 여러 단계로 하는 경우는 드물다. 실제로 다단계 테스트는 거의 항상 엔드투엔드 테스트의 범주에 속한다.)

 


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

 

# 모범 사례

1.  비관리 의존성에만 목 적용하기  (8장)

2.  시스템 끝에 있는 의존성에 대해 상호 작용 검증하기 

3.  통합 테스트에서만 목을 사용하고 단위 테스트에서는 하지 않기 

 

 

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

목을 사용할 때 시스템 끝에서 비관리 의존성과의 상호작용을 검증하라. 

그러면 회귀 방지를 극대화 할 수 있다.  (통합테스트로 검증된 코드가 더 많아지므로) 

 

 

즉 IMessageBus 보다  IBus 를 목으로 처리하면 회귀 방지가 좋아진다.

 

 

비관리 의존성에 대한 호출은 애플리케이션을 떠나기 전에 몇단계를 거친다. 마지막 단계를 선택하라. 

 

++

이제  IMessageBus를 목으로 처리하지 않게 되었다. 인터페이스를 두기에 타당한 이유는 목으로 처리하기 위한 것 뿐이므로 

이 인터페이스를 삭제하고 구체클래스인 MessageBus로 대체하자.  (8장과 이어지는 내용) 

 

 

# 목은 통합 테스트만을 위한 것 

 

7장 비즈니스로직과 오케스트레이션의 분리에서 비롯된다. 

코드가 복잡하거나 프로세스 외부 의존성과 통신할 수 있지만, 둘다는 아니다.

도메인 모델(복잡도 처리)  / 컨트롤러(통신처리)  두개의 계층 중, 

단위테스트는 도메인 모델에 속하므로 목을 사용할 필요가 없다. 

 

 

# 검증문 플루언트 인터페이스

fluent interface 를 사용하면 상호작용을 검증하는 것이 간결하고 표현력도 생긴다. 

또한 여러가지 검증을 묶을 수 있고 응집도가 높고 쉬운 영어 문장을 형성할 수 있다.

busSpy.ShouldSendNumberOfMessages(1)
      .WithEmailChangedMessage(user.UserId, "new@gmail.com")

 

* 플루언트 인터페이스: 메서드 체이닝을 기반으로 코드가 쉬운 영어문장으로 보이게끔 가독성을 향상시키는 API 설계 기법

* 메서드 체이닝: 메서드가 해당 객체를 반환하고 나서 반환된 객체를 다시 호출하는 식으로 여러 메서드를 한 번에 호출하는 기법

 

 

# 테스트 당 목이 하나일 필요가 없음

'단위' 용어는 코드 단위가 아니라 동작 단위를 의미한다.

동작 단위를 구현하는데 필요한 코드의 양은 관계가 없다. 

 

 

# 보유타입만 목으로 처리하기

서드파티 라이브러리 위에 항상 어댑터를 작성하고 기본 타입 대신 해당 어댑터를 목으로 처리해야한다.

 

아래와 같은 이점이 있기 때문이다. 

 

- 기본 라이브러리의 복잡성을 추상화하고

- 라이브러리에서 필요한 기능만 노출하며

- 프로젝트 도메인 언어를 사용해 수행할 수 있다. 

 

서드파티에서 깔끔한 인터페이스를 제공하더라도, 고유의 래퍼를 그위에 두는 것이 좋다.

라이브러리를 업그레이드할때 서드파티 코드가 어떻게 변경될지 알 수 없기 때문이다. 코드베이스 전체에 결쳐 파급효과가 일어날 수 있다.  추상계층을 두면 이러한 파급 효과를 하나의 클래스(어댑터) 로 제한할 수 있다. 

 

이 지침은 프로세스 내부 의존성에는 적용되지 않는다.

인메모리의존성, 관리의존성은 추상화할 필요 없다.

ORM이 외부애플리케이션에서 볼 수 없는 데이터를 접근해서 사용한다면 ORM도 추상화할 필요는 없다. 

 


10장 데이터 베이스 테스트

생략! (근데 모바일 개발자도읽어보면 좋은 내용이라 생각함)  

 


11장 단위 테스트 안티 패턴

 

# 비공개 메서드

단위테스트를 하려고 비공개 메서드를 노출하는 경우는 5장에서 다룬 기본 원칙 중 하나인

'식별할 수 있는 동작만 테스트' 하는 것을 위반한다.

 

비공개 메서드를 직접 테스트하는 대신, 포괄적인 식별할 수 있는 동작으로서 간접적으로 테스트하는 것이 좋다. 

 

 

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

도메인 지식을 테스트로 유출하는 것은 흔한 안티패턴이며 보통 복잡한 알고리즘을 다루는 테스트에서 일어난다. 

 

public static Calculator {

  public static int Add(int value1, int value2) {
       return value1 + value2
  }
}


public class CalculatorTests {

   public void Adding_two_numbers() {
       int value1 = 1;
       int value2 = 3;
       int expected = value1 + value3;  <-- 유출!! 
       
       int actual = Calculator.Add(value1, value2)
   
       Assert.Equal(expected, actual)
   }
}

 

단순히 제품 코드에서 복사-붙여넣기를 했을 뿐이다. 

이러한 테스트는 구현 세부사항과 결합되는 또 다른 예다. 리팩터링 내성 지표에서 거의 0점을 받게 되고 결국 가치가 없다.

 

그렇다면 어떻게 알고리즘을 올바르게 테스트할 수 있는가? 

테스트를 작성할때 특정 구현을 암시하지 말라.

알고리즘을 복제하는 대신 결과를 테스트에 하드코딩하라. 

 

하드코딩된 값의 중요한부분은 SUT가 아닌 다른 것을 사용해 미리계산한 것.

물론 알고리즘이 충분히 복잡한 경우에만 그렇다.  (모든 독자는 숫자 두개를 더하는데 있어서 전문가다.) 

 

 

# 코드 오염

코드오염은 테스트에만 필요한 제품 코드를 추가하는 것이다. 

(ex. 테스트환경임을 나타내고자 제품코드에 매개변수 추가하기)

 

테스트 코드를 제품코드와 분리해라.

인터페이스를 도입해서 두가지 구현을 생성하라. 

 

 

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

구체클래스를 목으로 처리하고 특정 메서드 (프로세스 외부 의존성을 호출하는 메서드) 만 재정의하는 케이스가 있다. 

이는 단일 책임원칙을 위반하는 결과다.

해당 클래스를 두가지 클래스 (도메인 로직이 있는 클래스 / 프로세스 외부 의존성과 통신하는 클래스) 로 분리하라. 

 

 

 

 

반응형
댓글