프로그래밍 책

단위 테스트를 잘 작성하는 방법 #좋은코드나쁜코드 #11장

열심히 사는 우진 2023. 4. 6. 19:59
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 11장을 기반으로 작성된 글입니다.

 

 

 

 


좋은 단위 테스트의 특징을 정리하면 다음과 같다.

  • 코드의 문제를 정확하게 감지한다.
  • 구현 세부 정보에 구애받지 않는다.
  • 실패가 잘 설명된다.
  • 테스트 코드가 이해하기 쉽다.
  • 테스트를 쉽고 빠르게 실행할 수 있다.

이를 위한 몇 가지 방법을 살펴보자.

모든 동작을 시험하라

함수 당 하나의 테스트 케이스만 있으면, 그 함수의 여러 동작을 테스트할 수 없다.

예컨대, 고객이 주택담보대출을 받을 자격이 있는지를 판단하는 함수가 있다고 하자.

  • 아래에 해당하면 기각한다.
    • 신용 등급이 좋지 않으면 거절한다.
    • 이미 대출이 있으면 거절한다.
    • 대출이 금지된 고객이면 거절한다.
  • 신청이 받아들여지면, 로직에 따라 금액을 산정한다.

위의 동작 각각을 모두 테스트해야 한다.

또한, 오류가 발생하는 시나리오, 예컨대 신청 액수가 음수인 경우와 같은 것들도 테스트해야 한다.

테스트만을 위해 퍼블릭으로 만들지 마라

  • 프라이빗 함수 테스트는 우리가 신경 쓰는 행동을 테스트하는 것이 아니다.
  • 구현 세부 사항에 독립적이지 못하다.
    • 리팩터링 후 실패할 수 있다.
  • 다른 개발자가 다른 곳에서 사용할 수 있다. 퍼블릭 API를 변경한 것과 마찬가지이다.

하지만 퍼블릭 함수 하나의 역할이 너무 비대해 테스트를 작성하기 어렵다면,

코드를 더 작은 단위로 분할하는 방법이 있다.

한 번에 하나의 동작만 테스트하라

여러 동작을 동시에 테스트하면,

  • 테스트를 이해하기 어렵고,
  • 실패 메시지가 명확하지 않아진다.

각 동작을 자체 테스트 케이스에서 테스트하면,

테스트 함수명과 로직을 통해 테스트를 이해하기 쉬울 뿐더러,

실패 메시지를 통해 혹은 테스트 함수명을 통해 실패 원인을 쉽게 알 수 있다.

매개변수를 사용한 테스트로, 하나의 동작에 대한 여러 경우를 동시에 테스트할 수도 있다.

자바의 Junit은 @ParameterizedTest로 매개변수 테스트가 가능하다.

공유 설정을 적절하게 사용하라

BeforeAll, BeforeEach, AfterAll, AfterEach 등을 통해 공유 설정을 실행하거나 해체할 수 있다.

BeforeAll 블록에 설정을 추가하는 것을 상태 공유,

BeforeEach 블록에 설정을 추가하는 것을 설정 공유 라고 한다.

상태 공유의 문제

이전에 실행된 테스트가 다른 테스트 케이스에 영향을 줄 수 있다.

따라서, 가능하다면 상태를 공유하지 않는 것이 좋다.

반드시 공유해야 한다면, AfterEach 블록을 통해 계속해서 새롭게 초기화해주는 것도 방법이다.

설정 공유의 문제

공유된 설정이 바뀌면, 사용하는 모든 테스트 케이스에 영향을 준다는 문제가 있다.

따라서 중요한 설정은 각각의 테스트 케이스 내에서 설정하는 것이 좋다.

설정 공유가 적절한 경우

하지만 뚜렷하게 반복되거나 비용이 많이 들어가는 설정은,

공유를 통해 반복과 비용을 줄일 수 있다.

테스트가 효과적이지 못하거나 파악하기 어려울 수 있다는 점을 명심한 채로,

심사숙고 후 사용할 필요가 있다.

적절한 어서션 확인자를 사용하라

assertThat(result.contains("custom_class_1")).isTrue();
assertThat(result.contains("custom_class_2")).isTrue();

테스트 케이스 testGetClassNames_containsCustomClassNames 실패 :
예상한 값은 true이지만 결과는 false입니다.

테스트의 의미는 맞지만 가독성이 좋지않고,

무엇보다 실패 메시지가 명확하지 않다.

asserThat(result).containsAtLeast("custom_class_1", "custom_class_2");

자바에서 지원하는 위와 같은 Assertion 확인자를 사용하면,

테스트 케이스 testGetClassNames_containsCustomClassNames 실패 :
Not true that
	[text-widget, selectable, custom_class_2[는
contains at least
	[custom_class_1, custom_class_2]를 포함하지 않습니다.
-------
빠진 항목: custom_class_1

위처럼 실패 메시지를 통해 실패 이유를 명확하게 알 수 있다.

테스트 용이성을 위해 의존성 주입을 사용하라

의존성 주입을 사용한 코드에서든, 테스트 더블을 사용하기 쉽다.

// 의존성을 하드코딩한 경우
class InvoiceReminder {
	private final AddressBook addressBook;
	private EmainSender emainSender;

	InvoiceReminder() {
		this.addressBook = DataStore.getAddressBook();
		this.emailSender = new EmailSenderImpl();
	}
	...
}

// 의존성을 외부에서 주입받는 경우
class InvoiceReminder {
	private final AddressBook addressBook;
	private EmainSender emainSender;

	InvoiceReminder(AddressBook addressBook, EmailSender emailSender) {
		this.addressBook = addressBook;
		this.emailSender = emailSender;
	}
	
	static InvoiceReminder create() { // 정적 팩토리 함수로, 의존성 주입 없이 생성된 인스턴스 제공
		return new InvoiceReminder(DataStore.getAddressBook(), new EmailSenderImpl());
	}
	...
}

의존성을 외부에서 주입받도록 코드를 작성하면, 아래와 같이 테스트 더블(Fake)을 사용할 수 있다.

InvoiceReminder invoiceReminder = new InvoiceReminder(addressBook, new FakeEmailSender());

테스트에 대한 몇 가지 결론

단위 테스트가 아닌 테스트 종류

  • 통합 테스트(integration test) : 여러 구성 요소, 하위 시스템, 모듈을 서로 연결하는 프로세스를 통합이라고 한다. 통합이 제대로 작동하는지 확인하기 위한 테스트이다.
  • 종단 간 테스트(end-to-end test) : 시작부터 끝까지 전체 소프트웨어 시스템을 통과하는 여정을 테스트한다.
    • 온라인 쇼핑몰 소프트웨어의 예 : 웹 브라우저 자동 구동 → 사용자가 구매를 완료하는 과정 → 구매 확인
  • 회귀 테스트 : 동작, 기능의 잘못된 변경은 없는지 정기적으로 수행하는 테스트
  • 골튼 테스트(특성화 테스트) : 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샷으로 저장 → 그것을 기반으로 테스트 수행 후, 코드가 생성한 출력이 다르면 테스트 실패
    • 신뢰성이 낮다
  • 퍼즈 테스트 : 무작위 값이나 특수한 값으로 코드를 호출하여, 코드 동작이 멈추지 않는지 점검

단위 테스트 뿐만 아니라, 다양한 테스트 기술, 유형, 수준에 대해 알아보고, 새로운 툴과 기술에 대한 최신 정보를 유지하는 것이 좋다.

반응형