본문 바로가기
정리

단위 테스트(Unit Testing) 핵심 정리, 1부

by baau 2025. 1. 20.

Unit Testing 단위테스트 : 생산성과 품질을 위한 단위 테스트 원칙과 패턴

  • 1부 : 더 큰 그림
  • 2부 : 개발자에게 도움이 되는 테스트 만들기
  • 3부 : 통합 테스트
  • 4부 : 단위 테스트 안티 패턴

 

1장. 단위 테스트의 목표

단위 테스트의 목표

단위 테스트의 목표는 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다.
코드베이스에서 무언가를 변경할 때마다 무질서도는 증가한다. 지속적인 정리와 리팩터링 등과 같은 적절한 관리가 없다면 시스템은 점점 더 복잡해지고 무질서해진다.
 
하나의 버그를 수정하면 더 많은 버그를 양산하고, 소프트웨어의 한 부분을 수정하면 다른 부분들이 고장난다.
테스트는 안전망 역할을 하며, 새로운 기능을 도입하거나 새로운 요구 사항에 더 맞게 리팩터링 한 후에도 기존 기능이 잘 동작하는지 확인하는 데 도움이 된다.
 
코드베이스를 지속적으로 검증하는 테스트 없이는 소프트웨어 개발이 쉽게 확장되지 않는다.
단위 테스트는  소프트웨어가 지속적으로 성장하게 하는 것이다.
 

좋은 테스트와 좋지 않은 테스트를 가르는 요인

모든 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 하지만, 그 밖에 다른 테스트는 그렇지 않다. (잘못된 경고, 오류 찾기와 유지 보수의 어려움)
 
지속 가능한 프로젝트 성장을 위해서는 고품질 테스트에만 집중해야 한다.
 

테스트 스위트 품질 측정을 위한 커버리지 지표

코드 커버리지 : 실행된 코드 라인 수와 제품 코드베이스의 전체 라인 수의 비율
분기 커버리지 : 수행하는 코드 분기 수와 제품 코드베이스의 전체 분기 수에 대한 비율 
 
코드 커버리지가 너무 적을 때 테스트가 충분치 않다는 좋은 증거가 될 수 있지만, 100% 커버리지라고 해서 반드시 양질의 테스트 스위트를 보장하지 않는다. 커버리지 숫자는 얼마든지 쉽게 장난칠 수 있다.
 
코드 커버리지보다는 분기 커버리지가 더 정확한 결과를 제공한다. 하지만 테스트 스위트의 품질을 결정하는 데 어떤 커버리지 지표도 의존할 수 없다.

  • 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다.
  • 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다.

그렇다면, 테스트 스위트의 품질을 어떻게 측정할까?
각 테스트를 하나씩 따로 평가하는 것뿐이다. 하지만 이는 꽤 큰 작업이며, 자동으로 확인할 수 없다.
하지만 성공적인 테스트 스위트는 다음과 같은 특성을 가지고 있다.

  • 모든 테스트는 개발 주기에 통합되어야 한다. 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 한다.
  • 코드베이스에서 가장 중요한 부분만을 대상으로 한다. 대부분 애플리케이션에서 가장 중요한 비즈니스 로직(도메인 모델)에 대한 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다.
  • 최소한의 유지비로 최대의 가치를 끌어낸다.

 

단위 테스트의 목표를 달성하기 위해서는 좋은 테스트와 좋지 않은 테스트를 구별하는 방법을 배우고, 테스트를 리팩터링 해서 더 가치 있게 만들어야 한다.


2장. 단위 테스트란 무엇인가

단위 테스트는 작은 코드 조각을 검증하고 빠르게 수행하고, 격리된 방식으로 처리하는 자동화된 테스트이다.
 

단위 테스트의 고전파와 런던파

 

격리 문제에 대한 접근
런던파는 테스트 대상 시스템을 협력자에게서 격리해야 한다고 주장한다. 따라서 대상 클래스에만 집중할 수 있도록 하나의 클래스가 다른 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야 한다.
하지만, 고전파는 드를 꼭 격리하는 방식으로 테스트하는 것이 아닌 단위 테스트끼리 서로 격리해서 실행되어야 한다고 주장한다. 이를 통해 테스트를 어떤 순서로든 가장 적합한 방식으로 실행할 수 있으며, 서로의 결과에 영향을 미치지 않아야 한다.
 
한 번에 한 클래스만 테스트하기
런던파 클래스를 단위로 간주한다. 테스트는 코드의 단위를 검증해서는 안된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수도 있다.
 
버그 위치 정확히 찾아내기
런던 스타일 테스트는 테스트가 실패하면 보통 SUT에 버그가 포함된 테스트만 실패하기 때문에 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있다. 하지만 고전적인 방식은 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 파급효과를 초래한다. 결국 문제의 원인을 찾기가 더 어려워질 수 있다. 하지만 이는 큰 문제가 되지 않는다. 테스트를 정기적으로 실행하면 버그의 원인을 알아낼 수 있다. 즉, 마지막으로 한 수정이 무엇인지 알기 때문에 문제를 찾는 것은 크게 어렵지 않을 것이다. 또한 테스트 스위트 전체에 걸쳐 계단식으로 실패하는 데 가치가 있다. 버그가 테스트 하나뿐만 아니라 많은 테스트에서 결함으로 이어진다면, 방금 고장 낸 코드 조각이 큰 가치가 있다는 것을 보여준다.
 
통합 테스트
런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주한다. 고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게 통합 테스트로 느껴질 것이다.


3장. 단위 테스트 구조

AAA 패턴

각 테스트를 준비, 실행, 검증으로 나누어 검증하는 테스트이다.

AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 가져, 일관성 있고 익숙해지면 모든 테스트를 쉽게 읽고 이해할 수 있다. 

  • 준비구절 : 테스트 대상 시스템과 해당 의존성을 원하는 상태로 만든다.
  • 실행 구절 : 테스트 대상 시스템에서 메서드를 호출하고 준비된 의존성을 전달하며 출력값을 캡처한다.
  • 검증 구절 : 결과를 검증한다. 결과는 반환 값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출된 메서드 등으로 표시될 수 있다.

만약 검증 구절로 구분된 여러 개의 실행 구절을 보면, 여러 개의 동작 단위를 검증하는 테스트로 생각되어진다.

이러한 테스트 구조는 피하는 것이 좋다. 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고 간단하고, 빠르며 이해하기 쉽다.

일련의 실행과 검증이 포함된 테스트를 보면 리펙터링 하고, 각 동작을 고유의 테스트로 도출해라.


If 문이 있는 단위테스트는 안티 패턴이다. 테스트는 분기가 없는 간단한 일련의 단계여야 한다. 이러한 테스트도 반드시 여러 테스트로 나눠야 한다.

 

준비 구절이 가장 큰 경우

  • 일반적으로 준비 구절은 세 구절 중 가장 크다.
  • 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다.
  • 오브젝트 마터와 테스트 데이터 빌더

 

실행 구절이 한 줄이상인 경우를 경계해라

  • 실행 구절은 보통 코드 한 줄이다.
  • 실행 구절이 두줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다.

 

검증 구절에는 검증문이 얼마나 있어야 하는가

  • 단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
  • SUT에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버를 정의하는 것이 좋다.

 

종료 단계는 어떤가

  • 테스트에 의해 작성된 파일을 지우거나 데이터베이스 연결을 종료하고자 이 구절을 사용할 수 있다.
  • 종료는 일반적으로 별도의 메서드로 도출돼, 클래스 내 모든 테스트에서 재사용된다. AAA 패턴에는 이 단계를 포함하지 않는다.

 

테스트 대상 시스템 구별하기

  • SUT는 테스트에서 중요한 역할을 하는데, 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공한다.
  • 동작은 여러 클래스에 걸쳐 있을 만큼 클 수도 있고 단일 메서드로 작을 수 있지만, 진입점은 오직 하나만 존재할 수 있다.
  • 따라서 SUT을 의존성과 구분하는 것은 중요하며, 테스트 대상을 찾는 데 시간을 너무 많이 들일 필요도 없기에 테스트 내 SUT 이름을 sut으로 하라.

 

준비, 실행, 검증 주석 제거하기

  • 테스트 내에서 특정 부분이 어떤 구절에 속해 있는지 파악하는 데 시간을 많이 들이지 않도록 세 구절을 서로 구분하는 것 역시 중요하다.
  • 각 구절을 시작하기 전에 주석을 달거나, 빈 줄을 추가하여 구분할 수 있다.
  • 만약 준비 단계에 빈 줄을 추가해 설정 단계를 구분하는 경우에는 구절 주석을 유지하는 것이 좋다.
  • 그렇지 않다면 빈 줄을 통해 각 구절을 구분하고, 주석을 제거해라.

 

테스트 간 테스트 픽스처 재사용

준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이다. 따라서 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다.

 

생성자에서 픽스처를 초기화 (테스트 간의 높은 결합도를 보여주는 안티 패턴)

  • 테스트의 준비 로직을 수정하면 클래스의 모든 테스트에 영향을 준다.
  • 테스트를 수정해도 다른 테스트에 영향을 주어서는 안 된다. 테스트는 서로 격리돼 실행해야 한다.
  • 테스트 클래스에 공유 상태를 두지 말아야 한다.

비공개 팩토리 메서드

  • 공통 초기화 코드를 비공개 팩토리 메서드로 추출해 테스트 코드를 짧게 하면서,
  • 동시에 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있다.
  • 매우 읽기 쉽고 재사용 가능한 메서드를 정의하여 가독성을 높일 수 있다.


단위 테스트 명명법

  • 엄격한 테스트 명명 정책을 시행하지 않아야 한다.
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 각 테스트의 이름을 지정하자.
  • 테스트 이름에서 밑줄 표시로 단어를 구분하고, 테스트 대상 메서드 이름을 넣지 말자.


매개변수화된 테스트 리팩터링 하기

  • 테스트 하나로는 동작 단위를 완전하게 설명하기에는 충분하지 않다.
  • 이 단위는 일반적으로 여러 구성 요소를 포함하여, 각 구성 요소는 자체 테스트로 캡처해야 한다.
  • 동작이 충분히 복잡하면, 이를 설명하는 데 테스트 수가 급격히 증가할 수 있으며 관리가 어려워질 것이다.
  • 하지만 대부분의 단위 테스트 프레임워크는 매개변수화 된 테스트 (parameterized test)를 사용해 유사한 테스트를 묶을 수 있는 기능을 제공한다.

 

검증문 라이브러리를 사용해 테스트 가독성 향상

  • 테스트 가독성을 높이기 위해 검증문 라이브러리를 사용하자.
  • Assert.Equal(30, result); 를 사용하는 것보다 result.should().Be(30); 을 사용하여 가독성을 높일 수 있다.