구글 엔지니어는 이렇게 일한다

12. 단위 테스트

주요 내용

  • 변하지 않는 테스트를 만들자
  • 공개 API를 통해 테스트하자
  • 상호작용이 아닌 상태를 테스트하자
  • 테스트는 완전하고 명확하게
  • 메서드가 아닌 행위를 테스트하자
  • 행위가 부각되게 테스트 하자
  • 테스트의 이름은 검사하는 행위가 잘 드러나도록 짓자
  • 테스트에는 로직을 포함하지 말자
  • 실패 메시지를 명확하게

[11장. 테스트 개요]에서는 테스트를 분류하는 주요 축인 크기와 범위를 소개했습니다. 크기란 테스트가 소비하는 자원과 수행할 수 있는 작업을 뜻하고, 범위는 테스트가 검증하고자 하는 코드의 양을 의미합니다.
구글은 작은 범위의 테스트인 단위 테스트를 선호합니다. 이는 버그를 예방할 뿐만 아니라 엔지니어의 생산성 개선에도 도움을 줍니다. 왜냐하면 작성하기 쉽고, 빠르고 결정적이어서 개발자들이 수시로 피드백 할 수 있으며, 실패시 원인 파악이 빠르기 때문입니다. 또한 단위 테스트는 문서자료나 예제 코드의 역할까지 수행합니다.
구글에서는 수없이 작성되고 실행되는 단위 테스트의 테스트 유지보수성(test maintainability)을 중시합니다. 이번 장에서는 이것이 무엇인지 알아보고 높은 유지보수성을 달성하는 기법들을 배워봅시다.

1. 유지보수하기 쉬워야 한다

깨지기 쉬운(bittle) 테스트
실제로 버그가 없음에도, 심지어 검증 대상과 관련 없는 변경 때문에 실패하는 테스트
불명확한(unclear) 테스트
무엇이 잘못되어 실패했는지, 어떻게 고쳐야 하는지를 파악하기 어려운 테스트

위와 같은 테스트를 작성했다면 아주 간단한 기능을 수정했을 뿐인데 자동 테스트 시스템이 오류를 뱉어내며 소위 말하는 억까를 시전할 수 있습니다. 이런 테스트는 원래의 의도와는 정반대의 효과를 내며 생산성을 저하시킵니다. 어떻게 하면 깨지기 쉬운 테스트를 예방하고 명확한 테스트를 작성할 수 있는지 다음에서 설명하겠습니다.

2. 깨지기 쉬운 테스트 예방

1) 변하지 않는 테스트로 만들기
이상적인 테스트라면 변하지 않아야 합니다. 한 번 작성한 후로 대상 시스템의 요구사항이 바뀌지 않는 한 절대 수정할 일이 없어야 합니다. 코드 변경 유형에는 다음의 네가지 유형들이 있는데, 각 유형별로 테스트가 어떻게 대응해야 하는지 따져보겠습니다.

  • 순수 리팩토링
    성능 최적화, 코드 가독성 개선 등 내부만 리팩토링하는 코드 변경입니다. 이 때는 테스트에 변경이 있으면 안됩니다. 만약 테스트 변경이 필요하다면 이는 순수 리팩토링을 한 것이 아니거나, 테스트의 추상화 수준이 적절하지 않았다는 뜻입니다.

  • 새로운 기능 추가
    새로운 기능이나 행위를 추가할 때는 기존 행위에 영향을 주지 않아야 합니다. 그러므로 기존 테스트의 변경 없이 새 기능을 검증할 테스트만 추가 됩니다.

  • 버그 수정
    버그가 존재한다는 것은 기존 테스트에 빠진 것이 있다는 의미입니다. 따라서 버그 수정과 함께 누락된 테스트를 추가해야 합니다. 새로운 기능추가와 비슷하게 기존의 테스트는 수정하지 않습니다.

  • 행위 변경
    시스템의 기존 행위를 변경하는 경우로 기존 테스트 역시 변경되어야 합니다.

즉, 순수 리팩토링, 새 기능 추가, 버그 수정은 기존 테스트를 수정할 일이 없어야 합니다. 시스템을 확장할 때는 기존 테스트들을 일일이 손보는 것이 아니라 확장한 부분과 관련된 소수의 테스트만 새로 작성하면 됩니다.

2) 공개 API를 사용해 테스트하기
공개 되지 않은(ex. private 메소드) 로직을 직접 테스트하는 것은 실제 사용자와는 매우 다르게 사용하는 것입니다. 이런 테스트는 깨지기 쉬워집니다. 하지만 공개 API만 사용해서 테스트한다면 정의상 대상 시스템을 사용자와 똑같은 방식으로 사용하는 것이므로 더 현실적이고 깨지지 않습니다.
공개 API란 코드 소유자가 third-party에 노출한 API를 의미합니다. 명확히 어디까지가 공개 API라고 규정할 수는 없지만 구글은 다음과 같은 팁을 공유합니다.

  • 소수에 대한 보조 메소드/클래스
    소수의 다른 클래스를 보조하는 용도인 메서드나 클래스라면 독립된 단위로 생각하지 않습니다. 이런 것들은 직접 테스트하지 말고 이들이 보조하는 클래스를 통해 우회적으로 테스트 합니다.

  • 누구나 접근 가능한 패키지/클래스
    소유자의 통제 없이 누구나 접근 가능한 패키지나 클래스라면 직접 테스트하는 단위로 취급해야합니다.

  • 접근 제한이 있고 다방면으로 보조적인 패키지/클래스
    소유자만 접근 가능하지만 다방면으로 유용한 기능을 제공하는 패키지나 클래스는 직접 테스트 하는 단위로 취급해야 합니다. 테스트 중복이 생길 수 있지만 이는 유익한 중복입니다.

이따금 "내부 구현을 직접 테스트 하는 것이 좋은 것 아니야?" 라고 의심하는 개발자들이 있습니다. 하지만 구글의 경험상 그것은 여러번 유지보수를 하도록 만드는 원인이 됩니다.

3) 상호작용이 아닌 상태를 테스트하기

- 상태 테스트
메서드 호출 후 시스템 자체를 관찰

- 상호작용 테스트
호출을 처리하는 과정에서 시스템이 다른 모듈들과 협력해 기대한 동작을 수행하는지 관찰

대체로 상호작용 테스트가 상태 테스트보다 깨지기 쉽습니다. 우리가 진짜 원하는 것은 결과가 무엇(WHAT)이냐지만, 상호작용 테스트는 결과에 도달하기까지 시스템이 어떻게(HOW) 동작하냐를 확인하려하기 때문입니다.
상호작용 테스트는 Mocking Framework에 지나치게 의존해서 만들어집니다. 진짜 객체가 빠르고 결정적이라면 테스트 대역을 지양하고 진짜 객체를 사용해야 합니다.

3. 명확한 테스트 작성

테스트가 실패하는 이유는 크게 두가지 입니다.

  • 대상 시스템에 문제가 있거나 불완전할 때
  • 테스트 자체에 결함이 있을 때

테스트가 실패했다면 둘 중 어느 부류인지 빠르게 파악하는 것이 중요합니다(=명확성). 그래서 명확한 테스트라 함은 존재 이유와 실패 원인을 엔지니어가 곧바로 알아차릴 수 있는 테스트를 말합니다. 명확한 테스트를 작성하는 방법은 다음과 같습니다.

1) 완전하고 간결하게 만들기

완전한 테스트
결과에 도달하기까지의 논리를 이해하는 데 필요한 모든 정보가 본문에 담겨있는 테스트
간결한 테스트
코드가 산만하지 않고, 관련 없는 정보는 포함하지 않은 테스트

두 특성 중 어느하나라도 담고 있지 않다면 안좋은 테스트입니다.

2) 메서드가 아니라 행위를 테스트하기

행위
특정 상태에서 특정한 일련의 입력을 받았을 때 시스템이 보장하는 반응

보통 메서드 하나 당 테스트 하나를 구현하려고 합니다. 하지만 행위를 기준으로 매서드를 바라보면 행위와 매서드는 다대다 관계입니다. 어떤 행위는 메서드 여러개를 연계해야 완성됩니다. 메서드에 얽메이지 않고 테스트를 구현해야 합니다.

행위 주도 테스트는 메서드 중심 테스트보다 명확합니다. 이유는 다음과 같습니다.

  • 자연어에 더 가깝게 읽히기 때문
  • 테스트 각각이 더 좁은 범위를 검사하기 때문
  • 각 테스트가 짧고 서술적이어서 이미 검사한 기능이 무엇인지 더 쉽게 확인할 수 있기 때문

테스트의 구조는 행위가 부각되도록 구성해야합니다. given/when/then으로 나누어 구성할 수 있습니다. assertion문을 코드 사이사이에 넣으면 읽기 어려워집니다. 이럴 땐 and문을 추가하거나 메서드를 쪼개야합니다. 테스트 각각은 하나의 행위만 검사해야 합니다.

또한 테스트의 이름은 길고 상세해도 좋으니 검사하는 행위에 맞게 지어야합니다. 같은 테스트 클래스 안에서만 일관적이라면 다양한 방식의 이름 짓기 전략도 괜찮습니다. should로 시작하는 이름을 사용하는 것도 좋은 방법입니다.

3) 테스트에 논리를 넣지 말기
테스트가 명확하면 검토하기 쉽습니다. 논리(연산자, 반복문, 조건문 등)가 포함되면 명확성을 해치게 됩니다. 스마트한 로직보다는 직설적인 코드를 고집해야 합니다. 또한 더 서술적이고 의미있는 테스트를 위해 약간의 중복은 허용하는 것이 좋습니다.

4) 실패 메시지를 명확하게 작성하기
실패 메시지는 문제의 원인을 잘 파악할 수 있도록 작성해야 합니다. 그래서 {원하는 결과}, {실제 결과}, {이때 받은 매개변수}를 포함해야 합니다.

[안좋은 예]
Test failed: account is closed
[좋은 예]
Expected an account is state CLOSED, but got account:
 {name: "my account", state: "OPEN"}

4. 테스트와 코드 공유: DRY가 아니라 DAMP!

DRY
Don't Repeat Yourself (반복하지 말라)
DAMP
Descriptive And Meaningful Phrase (서술적이고 의미있는 문구)

원래 프로그래밍에서는 DRY 원칙을 따릅니다. 이것이 더 유지보수하기 쉽기 때문입니다. 하지만 테스트 코드에서는 DAMP 원칙을 따라야 합니다. 테스트에서 DRY를 고집하면 명확성이 떨어지며, 테스트가 복잡해집니다. DAMP가 DRY를 대체하는 것이 아니라 보완하는 개념입니다. 테스트에서의 리팩토링은 반복을 줄이는 것이 아니라 더 서술적이고 의미 있게 하는 방향으로 이루어져야 한다는 것이 핵심입니다.

코드를 여러 테스트가 공유하는 패턴은 어떻게 구현해야 할까요? 다음의 세가지 분류로 알아보겠습니다.
1) 공유 값 (shared value)
테스트 작성자가 필요한 값들만 명시해 도우미 메서드에 요청하면 그 외 값들에는 적절한 기본값을 설정해서 반환하도록 합니다.

2) 공유 셋업 (shared setup)
setup 메서드란 테스트 스위트에 속한 테스트 각각을 수행하기 직전에 실행되는 메서드입니다. 대상 객체와 협력 객체(collaborator)를 생성하는데 유용합니다. 해당 객체를 생성할 때 쓰인 인자를 몰라도 되고, 테스트 수행 후에도 객체의 상태가 변하지 않을 때 유용합니다. 특정 값을 요구하는 테스트라면 코드가 반복되더라도 값을 재정의 해야 합니다.

3) 공유 도우미 메서드와 공유 검증 메서드
테스트용 값을 생성하는 생성할 때는 공유 도우미 메서드가 효율적입니다. 검증 메서드는 여러 조건을 확인하지 않고 하나의 목적에만 집중해야 합니다.

테스트 인프라
다른 테스트 스위트와 코드를 공유해 유용해지는 코드
통합 테스트나 end-to-end 테스트 때 유용하며, 의존하는 코드가 많다. 테스트 인프라는 독립된 제품 대우를 해줘야 하며 테스트 인프라를 검사할 자체 테스트가 필요하다.

profile
software engineer

0개의 댓글