[xUnit 테스트 패턴] 10장 - 결과 검증


10장 - 결과 검증

상태 검증

  • 기대 결과 값이 발생했는지 검증
    • SUT 실행 후 상태를 검사
  • 상태 검증 2가지 방법
    • 절차적 상태 검증
      • SUT의 최종 상태를 검출해 기대 값과 같은지 검증하는 단언문 작성
    • 기대 객체
      • 기대 상태를 단언문으로 표현

내장 단언문 사용하기

  • assertTure(aBooleanExpression) - 결과 지정 단언문
  • assertEquals(expected, actual) - 단순 동등 단언문
  • assertEquals(expected, actual, tolerance) - 퍼지 동등 단언문
  • 단언문 사용 시 주의점
    • 참이어야 하는 모든 것을 검증해야 함
    • 단언문의 문서로서의 가치
    • 테스트가 실패할 때 실패 메시지로 무엇이 문제인지를 바로 알 수 있으면 좋다.
      • 단언 메세지를 메시지 인자로 추가해줘야 한다.

델타 단언문

  • 새로 추가된 객체/로우만 검증
  • 불확실성에 대처하는 방법
    • 관련 테이블/클래스에 대한 ‘스냅샷’을 테스트 시작 시 저장
    • 테스트가 끝나면 생성된 객체/로우 집합에서 아까의 ‘스냅샷’을 제거 후 남은 것들을 기대 결과 값과 비교

외부 결과 검증

  • 예상 결과와 실제 결과를 파일로 저장한 후 외부 파일 비교 프로그램으로 차이를 확인 하는 것
  • 많이 바뀌지 않는 애플리케이션을 회귀 테스트하기 위해 자동화된 인수 테스트용으로 적합
  • 데이터가 많지 않을때에만 실용적

동작 검증

  • 동작은 동적이므로 상태 검증보다 훨씬 까다롭다.

절차형 동작 검증

  • SUT가 실행될 때 원하는 동작을 잡아낸 후 나중에 쓸 데이터를 저장해 둔다.
    • 나중에 테스트에서는 SUT의 출력 값을 해당 기대 결과 값과 하나하나 비교
  • 테스트에서 하나의(혹은 여러 단계의) 프로시저를 실행해 동작을 검증
  • 테스트에서는 SUT를 실행한 후 동작에 대한 기록을 얻어 단언문으로 검증

기대 동작 명세

  • 기대 동작은 보통 레이어-횡단 테스트와 연계해 객체나 컴포넌트의 간접 출력을 검증하는데 쓰인다.
  • SUT가 호출할 메소드가 있는 모의 객체를 설정해 SUT를 실행하기 전에 설치한다.

테스트 코드 중복 줄이기

  • 테스트 코드 중복은 깨지기 쉬운 테스트, 깨지시 쉬운 픽스처, 높은 테스트 유지 비용을 유발
  • 너무 많은 코드로 인해 애매한 테스트의 증상 일 수 있다.
  • 검증 개수를 줄이는 기법
    • 기대 객체(Expected Object)
    • 맞춤 단언문(Custon Assertion)
    • 검증 메소드(Verification Method)

기대 객체

  • 검증하려는 값들이 여러 변수에 저장된다면 적절한 클래스 객체를 하나 만들어 그 필드 값을 검증하러는 값들로 초기화 할 수 있다.
    • 필드 값만 비교할 수 있는 equals 메소드가 있고 기대 객체를 자유자재로 만들 수 있을 때 쓸 수 있다.
  • 만일 위와 같은 상황이 안될 때 방법
    • 맞춤 단언문을 구현
    • 기대 객체 클래스의 equals 메소드에 테스트용 동등을 따로 구현

맞춤 단언문

  • 직접 작성해야 하는 도메인 전용 단언문
  • 결과 검증 절차를 알기 쉬운 이름 밑에 숨겨줘 결과 검증 로직의 의도를 좀 더 잘 알 수 있게 한다.
  • 많은 코드를 제거하므로 애매한 테스트를 막을 수 있다.
static void assertLineItemsEqual(String msg, LineItem exp, LineItem act){
  assertEquals(msg + "Inv", exp.getInv(), act.getInv());
  assertEquals(msg + "Prod", exp.getProd(), act.getProd());
  assertEquals(msg + "Quan", exp.getQuantity(), act.getQuantity());
}
  • 맞춤 단언문을 만드는 두가지 방법
    1. 기존의 복잡한 테스트 코드를 리팩토링 한다.
    2. 비어있는 단언 메소드를 호출하게 테스트를 작성하고, 필요한 곳들이 파악되면 비어있는 단언 메소드에 적당한 로직을 채워 넣는다.

결과를 설명하는 검증 메소드

void assertInvoiceContainsOnlyThisLineItem(Invoice inv, LineItem expItem){
  List lineItems = inv.getLineItems();
  assertEquals("number of items", lineItems.size(), 1);
  LineItem actual = (LineItem)lineItems.get(0);
  assertLineItemsEqual("", expItem, actual);
}
  • 검증 메소드와 맞춤 단언문의 차이점
    • 맞춤 단언문
      • 단언만 함
      • 시그니처는 일반적인 동등 단언문 시그니처(assertSomething(message, expected, actual))
    • 검증 메소드
      • SUT와 상호작용
      • SUT에게 넘겨줄 인자가 필요하므로 형태가 자유분방

인자를 받는 테스트와 데이터 주도 테스트

  • 인자를 받는 테스트
    • 픽스처 설치, SUT 실행, 결과 검증 부분을 뽑아내서 만든다.
    • 테스트용 데이터를 테스트 메소드에서 받는다.
  • 데이터 주도 테스트
    • 자체가 테스트 메소드이고 파일에서 테스트용 데이터를 직접 읽어 들인다.

테스트 내 조건문 로직 피하기

  • 테스트 메소드의 코드는 테스트 할 수 없으므로 테스트 내 조건문 로직이 있을 경우 테스트에 대한 신뢰가 떨어짐

if문 제거하기

  • 보호 단언문 사용
//보호 단언문 사용 전
Line lineItems = invoice.getLineItems();
if(lineItems.size() == 1){
  LineItem expected = new LineItem(invoice, product, 5, new BigDecimal("30"), new BigDecimal("69.96"));
  LineItem actItem = (LineItem) lineItems.get(0);
  assertEquals("invoice", expected, actItem);
} else {
  fail("Invoice should have exactly one line item");
}
//보호 단언문 사용
Line lineItems = invoice.getLineItems();
assertEquals("number of items", lineItems.size(), 1);
LineItem expected = new LineItem(invoice, product, 5, new BigDecimal("30"), new BigDecimal("69.96"));
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("invoice", expected, actItem);
  • 보호 단언문의 장점은 테스트 내 조건문 로직이 없어도 테스트 에러를 낼 수 있는 곳에 단언문이 걸리게 할 수 있다는 점

반복문 제거하기

  • SUT가 리턴한 컬렉션의 내용을 기대 값과 비교하는 반복문 형태로 테스트 내 조건문 로직이 나타날 수 있다.
  • 문제점 3 가지
    • 반복문 코드를 완전 자동 테스트로 테스트 할 수 없으므로 테스트 할 수 없는 테스트 코드가 추가 되는 셈
    • 의도를 바로 알기 어렵기 때문에 애매한 테스트가 되기 쉬움
    • 반복문 작성이 복잡하다보니 개발자가 자체 검사 테스트를 쓰려하지 않아 테스트를 작성하지 않는 개발자가 될 수 있다.

기타 기법

거꾸로, 밖에서 안으로 작업하기

  • 거꾸로 작업하기란 단언문들을 먼저 작성하는 걸 의미
    • 단언문의 의도를 보여줄 수 있게 적절한 이름의 지역 변수 값을 단언한다.
    • 단언문을 실행하기 위해 채워 넣어야 하는 테스트 코드를 작성한다.
    • 즉, 단언문 변수를 먼저 선언하고 그 후에 적절한 내용으로 값들을 초기화 하는 식
  • 밖에서 안으로 또는 위에서 아래로 작업은 일정한 추상 수준을 유지하는 것을 의미
    • 테스트 유틸리티 메소드를 호출하는 방식으로 테스트를 작성하는 동안에는 SUT의 요구 사항에만 집중하게 한다.
    • 객체를 어떻게 생성할지, 결과를 어떻게 검증할지 고민하지 말고 어떤 객체나 결과가 필요한지만 작성 해 둔다.
    • 정의하지 않은 채로 먼저 사용하는 유틸리티 메소드는 테스트 자동 로직이 들어갈 장소 역할을 하게 된다.

테스트 주도 개발로 테스트 유틸리티 메소드 작성하기

  • 테스트 유틸리티 메소드를 사용하는 테스트 메소드 작성이 끝나면 테스트 유틸리티 메소드를 구현할 시점이다.
  • 테스트 유틸리티 메소드의 동작을 검증하는 단위 테스트는 금방 작성하고, 테스트 유틸리티 메소드에 대한 신뢰도 높아진다.
  • 필요하지도 않은 경우까지 처리할 수 있게 일반적인 로직을 구현할 필요가 없다.

재사용 가능한 검증 로직을 둘 위치

  • 테스트 케이스 클래스 안쪽에 위치
  • 테스트 케이스 상위 클래스로 메소드 올리기를 하거나 테스트 도우미 안으로 메소드 옮기기를 하면 재사용성이 올라감