6장, 단위 테스트 스타일

2022.04.05

6.1 단위 테스트의 세 가지 스타일

단위 테스트는 세 가지 스타일이 있다.

  • 출력 기반 테스트(output-based testing)
    • 세 가지 중 가장 품질이 좋다.
    • 아무데서나 사용할 수 없으며, 순수 함수 방식으로 작성된 코드에만 적용된다.
  • 상태 기반 테스트(state-based testing)
    • 두번 째로 좋은 선택.
  • 통신 기반 테스트(communication-based testing)
    • 간헐적으로 사용해야 한다.

6.1.1 출력 기반 테스트 정의

출력 기반 스타일은 SUT에 입력을 넣고 생성되는 출력을 점검하는 방식이다.
이러한 스타일은 전역 상태나 내부 상태를 변경하지 않는 코드에만 적용되므로 반환 값만 검증하면 된다.


출력 기반 단위 테스트 스타일은 함수형(functional)이라고도 한다.
부작용 없는 코드 선호를 강조하는 프로그래밍 방식인 함수형 프로그래밍에 뿌리를 두고 있다.

6.1.2 상태 기반 스타일 정의

상태 기반 스타일은 작업이 완료된 후 시스템 상태를 확인하는 것이다.
상태라는 용어는 SUT나 협력자 중 하나, 또는 데이터베이스나 파일 시스템 등과 같은 프레시스 외부 의존성의 상태 등을 의미할 수 있다.


6.1.3 통신 기반 스타일 정의

통신 기반 스타일은 목을 사용해 SUT와 협렵자 간의 통신을 검증한다.


6.2 단위 테스트 스타일 비교

좋은 단위 테스트의 4대 요소를 통해 각 스타일을 비교해 보자.

  • 회귀 방지
  • 리팩터링 내성
  • 빠른 피드백
  • 유지 보수성

6.2.1 회귀 방지와 피드백 속도 지표로 스타일 비교하기

테스트 스타일과 테스트 피드백 속도 사이에는 상관관계가 거의 없다.
테스트가 프로세스 외부 의존성과 떨어져 단위 테스트 영역에 있는 한, 모든 스타일은 테스트 실행 속도가 거의 동일하다.
목은 런타임에 지연 시간이 생기는 편이므로 통신. 기반 테스트가 약간 나쁠 수 있다.
그러나 이러한 테스트가 수만개 수준이 아니라면 별 차이는 없다.

6.2.2 리팩터링 내성 지표로 스타일 비교하기

출력 기반 테스트는 테스트가 테스트 대상 메서드에만 결합되므로 거짓 양성 방지가 가장 우수하다.
테스트가 구현 세부 사항에 결합하는 경우는 테스트 대상 메서드가 구현 세부 사항일 때뿐이다.

상태 기반 테스트는 일반적으로 거짓 양성이 되기 쉽다.
이러한 테스트는 테스트 대상 메서드 외에도 클래스 상태와 함께 작동한다.

통신 기반 테스트가 허위 경보에 가장 취약하다.
테스트 대역으로 상호 작용을 확인하는 테스트는 대부분 깨지기 쉽다.

피상적인 테스트가 통신 기반 테스트의 결정적인 특징이 아닌 것처럼, 불안정성도 통신 기반 테스트의 결정적인 특징이 아니다.
캡슐화를 잘 지키고 테스트를 식별할 수 있는 동작에만 결합하면 거짓 양성을 최소로 줄일 수 있다.

6.2.3 유지 보수성 지표로 스타일 비교하기

유지 보수성은 단위 테스트의 유지비를 측정하며, 다음 두 가지 특성으로 정의한다.

  • 테스트를 이해하기 얼마나 어려운가?
    • 테스트 크기에 대한 함수
  • 테스트를 실행하기 얼마나 어려운가?
    • 테스트에 직접적으로 관련 있는 프로세스 외부 의존성 개수에 대한 함수

출력 기반 테스트의 유지 보수성

다른 두 가지 스타일과 비교하면, 출력 기반 테스트가 가장 유지 보수하기 용이하다.
출력 기반 테스트는 거의 항상 짧고 간결하므로 유지 보수가 쉽다.

출력 기반 테스트의 기반 코드는 전역 상태나 내부 상태를 변경할 리 없으므로, 프로세스 외부 의존성을 다루지 않는다.

상태 기반 테스트의 유지 보수성

상태 기반 테스트는 일반적으로 출력 기반 테스트보다 유지 보수가 쉽지 않다.
상태 검증은 종종 출력 검증보다 더 많은 공간을 차지하기 때문이다.

[Fact]
public void Adding_a_comment_to_an_article()
{
  var sut = new Article();
  var text = "Comment Text";
  var author = "John Doe";
  var now = new DateTime(2019, 4, 1);

  sut.addCoomment(text, author, now);

  // 글의 상태를 검증
  Assert.Eqaul(1, sut.Comments.Count);
  Assert.Eqaul(text, sut.Comments[0].Text);
  Assert.Eqaul(author, sut.Comments[0].Author);
  Assert.Eqaul(now, sut.Comments[0].DateCreated);

  // 헬퍼 메서드를 사용한 검증
  sut.ShouldContainNumberOfComments(1)
    .WithComment(text, author, now);

  // 검증문 라이브러리를 써서 테스트
  sut.Comments.Should().BeEquivalentTo(comment);
}

상태 기반 테스트는 훨씬 많은 데이터를 확인해야 하므로 테스트 크기가 대폭 커질 수 있다.
대부분 코드를 숨기고 테스트를 단축하는 헬퍼 메서드를 사용할 순 있지만 이러한 메서드 작성과 유지에도 상당한 노력이 필요하다.

또, 검증문 라이브러리를 써서 테스트를 단순하게 할 수 있지만 클래스가 값 객체로 변환할 수 있을 때만 효과적이고, 코드 오염(code pullution)(단지 단위 테스트를 가능하게 하거나 단순화하기 위한 목적만으로 제품 코드베이스를 오염시키는 것)으로 이어질 가능성이 있다.

위와 같은 기법을 적용할 수 있더라도 상태 기반 테스트는 출력 기반 테스트보다 공간을 더 많이 차지하므로 유지 보수성이 떨어진다.

통신 기반 테스트의 유지 보수성

통신 기반 테스트는 유지 보수성 지표에서 출력 기반 테스트와 상태 기반 테스트보다 점수가 낮다.
테스트 대역과 상호 작용 검증을 설정해야 하며, 이는 공간을 많이 차지한다.

6.2.4 스타일 비교하기: 결론

출력 기반 테스트가 가장 결과가 좋다.
구현 세부 사항과 거의 결합되지 않으므로 리팩터링 내성을 적절히 유지하고자 주의를 많이 기울일 필요가 없다.

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

출력 기반 스타일은 함수형으로 작성된 코드에만 적용할 수 있고, 대부분의 객체지향 프로그래밍 언어에는 해당하지 않는다.
코드를 순수 함수로 만들면 상태 기반 테스트나 통신 기반 테스트 대신 출력 기반 테스트가 가능해진다.

6.3 함수형 아키텍처 이해

6.3.1 함수형 프로그래밍이란?

함수형 프로그래밍은 수학적 함수(mathematical function, 순수 함수(pure function)라고도 함)를 사용한 프로그래밍이다. 수학적 함수는 숨은 입출력이 없는 함수이다.

입출력을 명시한 수학적 함수는 이에 따르는 테스트가 짧고 간결하며 이해학소 유지 보수하기 쉬우므로 테스트하기가 매우 쉽다. 출력 기반 테스트를 적용 할 수 있는 메서드 유형은 수학적 함수뿐이다. 이는 유지 보수성이 뛰어나고 거짓 양성 빈도가 낮다.

숨은 입출력의 유형은 아래와 같다.

  • 부작용 : 부작용은 메서드 시그니처에 표시되지 않은 출력이며, 따라서 숨어있다.
  • 예외 : 메서드가 예외를 던지면, 프로그램 흐름에 메서드 시그니처에 설정된 계약을 우회하는 경로를 만든다.
  • 내외부 상태에 대한 참조 : 메서드 시그니처에 없는 실행 흐름에 대한 입력들. 따라서 숨어 있다.
    • Date.now와 같이 정적 속성을 사용해 현재 날짜와 시간을 가져오는 메서드
    • 데이터베이스에서 데이터를 질의.

메서드가 수학적 함수 인지 판별하는 가장 좋은 방법은 프로그램의 동작을 변경하지 않고 해당 메서드에 대한 호출을 반환 값으로 대체할 수 있는지 확인하는 것이다. 메서드 호출을 해당 값으로 바꾸는 것을 참조 투명성(referential transparency)이라고 한다.

Public int Increment(int x) {
  return x + 1;
}

// 위의 메서드는 수학적 함수이다.다음 두 구문은 서로 동일하다.

int y = Increment(4);
int y = 5;

6.3.2 함수형 아키텍처란?

어떤 부작용도 일으키지 않는 애플리케이션을 만들 수는 없다. 이러한 것은 비현실적이다.
결국 부작용은 사용자 정보 업데이트, 장바구니에 새로운 주문 추가 등 모든 애플리케이션이 만들어내는 것이다.

함수형 프로그래밍의 목표는 부작용을 완전히 제거하는 것이 아니라 비지니스 로직을 처리하는 코드와 부작용을 일으키는 코드를 분리하는 것이다.

함수형 아키텍처는 부작용을 다루는 코드를 최소화하면서 순수 함수(불변) 방식으로 작성한 코드의 양을 극대화한다.
> 불변(immutable)이란 변하지 않는 것을 의미한다.
일단 객체가 생성되면 그 상태는 바꿀 수 없다.

  • 결정을 내리는 코드: 이 코드는 부작용이 필요 없기 때문에 수학적 함수를 사용해 작성할 수 있다.
  • 해당 결정에 따라 작용하는 코드: 이 코드는 수학적 함수에 의해 이뤄진 모든 결정을 데이터베이스의 변경이나 메시지 버스로 전송된 메시지와 같이 가시적인 부분으로 변환한다.

결정을 내리는 코드는 종종 함수형 코어, functional core(불변 코어, immutable core 라고도 함)라고도 한다.
해당 결정에 따라 작용하는 코드는 가변 셸, mutable shell이다.


6.3.3 함수형 아키텍처와 육각형 아키텍처 비교

함수형 아키텍처와 육각형 아키텍처는 비슷한 점이 많다. 둘 다 관심사 분리라는 아이디어를 기반으로 한다.

육각형 아키텍처는 도메인 계층과 애플리케이션 서비스 계층을 구별한다.
도메인 계층은 비지니스 로직에 책임이 있는 반면, 애플리케이션 서비스 계층은 데이터베이스나 SMTP 서비스와 같이 외부 애플리케이션과의 통신에 책임이 있다.
이는 결정과 실행을 분리하는 함수형 아키텍처와 매우 유사하다.

또 다른 유사점은 의존성 간의 단방향 흐름이다.
육각형 아키텍처에서 도메인 계층 내 클래스는 서로에게만 의존해야 한다.
함수형 아키텍처의 불변 코어는 가변 셸에 의존하지 않는다.

둘의 차이점은 부작용에 대한 처리에 있다.
함수형 아키텍처는 모든 부작용을 불변 코어에서 비지니스 연산 가장자리(가변 셸)로 밀어낸다.
반면, 육각형 아키텍처는 도메인 계층에 제한하는 한, 도메인 계층으로 인한 부작용도 문제 없다.
모든 수정 사항은 도메인 계층 내에 있어야 하며, 계층의 경계를 넘어서는 안 된다.

함수형 아키텍처는 육각형 아키텍처의 하위 집합이다.
극단적으로는 함수형 아키텍처를 육각형 아키텍처로 볼 수도 있다.

요약

  • 출력 기반 테스트는 SUT에 입력을 주고 출력을 확인하는 테스트 스타일이다.
  • 상태 기반 테스트는 작업이 완료된 후의 시스템 상태를 확인한다.
  • 통신 기반 테스트는 목을 사용해서 테스트 대상 시스템과 협력자 간의 통신을 검증한다.
  • 출력 기반 테스트가 테스트 품질이 가장 좋다. 구현 세부 사항에 결합되지 앟ㄴ으므로 리팩터링 내성이 있다. 또한 작고 간결하므로 유지 보수하기도 쉽다.
  • 상태 기반 테스트는 안전성을 위해 더 신중해야 한다. 단위 테스트를 하려면 비공개 상태를 노출하지 않도록 해야 한다.
    • 상태 기반 테스트는 크기가 큰 편이므로 유지 보수가 쉽지 않다. 헬퍼 메서드와 값 객체를 사용해 유지보수 문제를 완화할 수도 있지만 제거할 수는 없다.
  • 통신 기반 테스트도 안전성을 위해 더 신중해야 한다. 애플리케이션 경계를 넘어서 외부 환경에 부작용이 보이는 통씬만 확인해야 한다.
  • 함수형 프로그래밍의 목표는 비지니스 로직과 부작용을 분리하는 것이다.
  • 함수형 아키텍처는 모든 코드를 함수형 코어와 가변 셸이라는 두 가지 범주로 나눈다. 가변 셸은 입력 데이터를 함수형 코어에 공급하고, 코어가 내린 결정을 부작용으로 변환한다.
  • 함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성 사이의 절충이며, 함수형 아키텍처는 유지 보수성 향상을 위해 성능을 희생한다.
  • 모든 코드베이스를 함수형 아키텍처로 전환할 수는 없다. 함수형 아키텍처를 전략적으로 적용하라. 시스템의 복잡도와 중요성을 고려하라.