[xUnit 테스트 패턴] 15장 - 코드 냄새


15장 - 코드 냄새

애매한 테스트

증상 : 애매한 테스트

  • 테스트를 한눈에 이해하기 어렵다.

미치는 영향 : 애매한 테스트

  • 이해하기 어렵고 유지 보수하기도 어렵다.
  • 문서로서의 테스트 만들기 어렵고, 높은 테스트 유지 비용이 발생 할 수 있다.
  • 버그 투성이 테스트, 욕심쟁이 테스트가 될 수 있다.

원인 : 애매한 테스트

  • 테스트 메소드에 정보가 너무 많거나 적을 때 발생
  • 욕심쟁이 테스트
    • 하나의 테스트 메소드에 너무 많은 기능을 검증하려는 테스트
  • 미스터리한 손님
    • 픽스처와 검증 로직의 일부가 테스트 바깥에 있어 코드에서 이들 간의 인과 관계가 보이지 않는 테스트
  • 장황한 테스트(Verbose Test)의 일반적인 문제점
    • 일반 픽스처 - 필요 이상으로 큰 픽스처 생성 or 참조
    • 관련 없는 정보
    • 하드 코딩된 테스트 데이터
    • 간접 테스트

원인 : 욕심쟁이 테스트

  • 하나의 테스트 메소드에서 너무 많은 기능을 검증하려는 테스트
증상 : 욕심쟁이 테스트
  • 없는 거 빼고 전부 다 검증
  • 어디까지가 픽스처 설치, 어디서부터 SUT 실행인지 알기 어려움
    • SUT 실행 및 검증을 여러 단계에 거쳐서 함
근본 원인 : 욕심쟁이 테스트
  • 수동 테스트 시 테스트할 때마나 설치하는 고생을 줄이기 위해 별개의 테스트 조건들이라도 하나의 테스트 케이스에 전부 몰아넣어도 된다.
해결책 : 욕심쟁이 테스트
  • 자동화 된 테스트라면 결함 국소화를 위해 단일 조건 테스트 스위트로 만드는게 낫다.

원인 : 미스터리한 손님

  • 픽스처와 검증 로직의 일부가 테스트 바깥에 있어 코드에서 이들 간의 인과 관계가 보이지 않는 테스트
증상 : 미스터리한 손님
  • 픽스처 설치나 테스트의 결과 검증부가 테스트에서는 볼 수 없는 정보에 의존하고 검증하려는 동작을 이해하기 어렵다면 미스터리한 손님 문제가 있는 것
    • ex) 외부 파일(excel, csv 등) 로드에 의한 검증
미치는 영향 : 미스터리한 손님
  • 문서로서의 테스트 역할 할 수 없음
  • 누군가가 외부 자원을 수정하거나 지울 수 있다.
    • 자원 낙관주의(Resource Optimism))
  • 공유 픽스처라면 다른 테스트에서 픽스처 수정 시 변덕스러운 테스트가 될 수 있다.
근본 원인 : 미스터리한 손님
  • 외부 자원에 테스트가 의존
    • SUT 메소드에 전달되는 외부 파일 이름, 파일 내용이 SUT의 동작을 결정
    • 리터럴 키로 식별되는 데이터베이스 레코드의 내용을 읽어 객체에 쓴 뒤 이를 테스트에서 사용 or SUT에 전달
    • 파일에서 읽어 들인 내용을 단언 메소드 호출에 사용
    • 설치 데코레이터(Setup Decorator)로 공유 픽스처를 만들고 결과 검증 로직에서는 공유 픽스처의 객체들을 변수로 참조
    • 암묵적 설치(Implicit Setup)로 일반 픽스처를 설치하고 테스트 메소드에서 인스턴스 변수나 클래스 변수로 접근
해결책 : 미스터리한 손님
  • 인라인 설치로 신선한 픽스처를 쓰는 방법
    • 파일을 쓰는 경우
      • 파일 내용을 테스트에 문자열로 저장하고 그 내용을 파일에 다시 쓰거나 픽스처 설치 시 파일 시스템 테스트 스텝에 둘 수 있다.
    • 공유 픽스처나 암묵적 설치를 쓰는 경우
      • 픽스처 안의 객체들에 찾기 메소드로 접근
      • 파일을 써야 하면 파일 이름과 디렉토리로 어떤 데이터가 들어있는지 짐작할 수 있게 함

원인 : 일반 픽스처

  • 테스트에서 기능 검증에 필요 이상으로 큰 픽스처를 생성하거나 참조
근본 원인 : 일반 픽스처
  • 여러 테스트를 지원하기 위해 만든 픽스처를 쓰는 경우
  • 다른 픽스처가 필요한 테스트에서 암묵적 설치나 공유 픽스처를 쓰는 경우
  • 모든 테스트의 요구 사항을 맞춰야 하는 표준 픽스처를 쓰기 때문
미치는 영향 : 일반 픽스처
  • 문서로서의 테스트를 달성하기 어렵게 된다.
  • 깨지기 쉬운 픽스처가 되기 쉽다.
  • 느린 테스트가 될 수 있다.
해결책 : 일반 픽스처
  • 최소 픽스처를 써야 한다.
  • 신선한 픽스처를 써야 한다.
  • 공유 픽스처를 써야 한다면 테스트별로 가상의 데이터베이스 샌드박스를 만드는것을 고려

원인 : 관련 없는 정보

  • SUT의 동작에 영향을 미치는 것이 무엇인지 코드만 봐서는 알기 어렵다.
증상 : 관련 없는 정보
  • 객체에 전달되는 값 중 무엇이 기대 출력에 영향을 미치는지 알기 어렵다.
    • ex) 객체에 값을 여러개 설정해야 하고 참조 객체도 설정해야 하는 경우
  • 테스트 선조건을 알 수 없게 돼 테스트가 무엇을 검증하려는지 알기 어렵다.
근본 원인 : 관련 없는 정보
  • 테스트에 리터럴 값이나 변수 같은 데이터가 너무 많이 들어 있는 경우
  • 절차형 상태 검증을 써서 결과 검증에 필요한 모든 코드를 포함 시키는 경우
미치는 영향 : 관련 없는 정보
  • 문서로서의 테스트 달성하기 어렵다.
  • 높은 테스트 유지 비용 발생
  • 버그투성이 테스트 생길 가능성 높다.
해결책 : 관련 없는 정보
  • 관련 있는 정보만 인자로 받는 인자를 받는 생성 메소드 호출
  • 테스트에 중요하지 않은 값은 생성 메소드의 기본값 지정 or 더미 객체로 교체
  • 맞춤 단언문 사용

원인 : 하드 코딩된 테스트 데이터

  • 픽스처, 단언문, SUT 인자값이 테스트 메소드에 하드 코딩돼 있어 입력과 기대 결과 값 사이의 인과 관계가 애매함.
증상 : 하드 코딩된 테스트 데이터
  • 어떤 값들이 SUT의 동작에 영향을 미치는지 알기 어렵다.
  • 변덕스러운 테스트 같은 동작 냄새가 날 수 있다.
  • 리터럴 값(literal value) 사용
근본 원인 : 하드 코딩된 테스트 데이터
  • 테스트에 상관 없는 리터럴 값이 너무 많으면 발생
  • 잘라 붙여넣기로 테스트 로직 재사용 시 리터럴 값도 같이 복사하게 됨
미치는 영향 : 하드 코딩된 테스트 데이터
  • 문서로서의 테스트 달성하기 어려움
  • 공유 픽스처로 쓸 경우 발생
해결책 : 하드 코딩된 테스트 데이터
  • 리터럴 상수를 다른 걸로 바꾸기
    • 이름 있는 상수 값으로 바꾸기
  • 인자값을 기초로 결과 검증 로직에 쓰이는 값 사용 시 파생 값 사용
  • 공유 픽스처를 쓰고 있다면 별개의 생성 값을 써야 한다.
    • ex) unique key, random value

원인 : 간접 테스팅

  • 테스트 메소드와 SUT가 다른 객체를 통해 간접적으로 상호작용 하는 경우
증상 : 간접 테스팅
  • 프레젠테이션 레이어를 통해 비즈니스 로직을 테스트하는 경우
미치는 영향 : 간접 테스팅
  • 깨질 수 있는 모든 곳을 테스트하기가 불가능
  • 문서로서의 테스트 달성하기 어려움
  • 깨지기 쉬운 테스트가 되기 쉽다.
근본 원인 : 간접 테스팅
  • 테스트에서 접근하려는 클래스의 SUT 부분이 private 일 수 있다.
    • 테스트 용이성을 감안하지 않고 소프트웨어를 설계를 한 것
해결책 : 간접 테스팅
  • SUT의 테스트 용이성을 위한 설계를 개선해야 함
    • 테스트 가능한 컴포넌트 뽑아내기로 리팩토링
  • 리팩토링 불가능 할 때
    • 테스트 유틸리티 메소드 안에 캡슐화
    • 픽스처 설치는 생성 메소드에 위치
    • 결과 검증은 검증 메소드에 위치

해결책 패턴

  • 테스트 유틸리티 메소드를 호출
  • 이미 만들어 놓은 테스트 픽스처나 인자를 받는 테스트에 넘기는 식으로 테스트 로직 재사용
  • 테스트를 밖에서 안으로 방식으로 만들면 애매한 테스트를 줄일 수 있다.

테스트 내 조건문 로직

  • 테스트에 실행 안 될 수도 있는 코드가 있다.
  • 해결책
    • 테스트 메소드를 테스트가 필요 없을 정도로 최대한 단순하게 만들기

증상 : 테스트 내 조건문 로직

  • 테스트 메소드 안에 제어문이 들어 있는 경우
  • 높은 테스트 유지 비용 동작 증상 발생 시킴

미치는 영향 : 테스트 내 조건문 로직

  • 실행 경로가 다양한 코드는 훨씬 어렵고, 결과를 확신할 수도 없다.
  • 디버깅이 어렵다.
  • 어려운 작업에 대한 테스트를 정확하게 작성하기 어렵게 만든다.

원인 : 테스트 내 조건문 로직

  • 특정 테스트 코드 실행을 피하기 위해 if문 사용
  • 반복문 사용해서 검증, 애매한 테스트도 될 수 있다.
  • 다형성 데이터 구조를 검증하기 위해 테스트 내 조건문 로직 사용, 외부 메소드로 equals 메소드를 구현
  • 하나의 테스트에서 여러 다른 경우를 검증하고자 할 때 - 유연한 테스트
  • 존재하지 않는 픽스처 객체를 해체하지 않으려고 if문 사용

원인 : 유연한 테스트

  • 언제, 어디에서 실행되느냐에 따라 다른 기능을 검증하는 테스트 코드
증상 : 유연한 테스트
  • 테스트 외부 요인에 따라 다른 기대 결과를 생성하고 싶을 때 사용
    • ex) 현재 시각을 받아 SUT의 결과가 무엇이어야 하는지 결정
    • ex) 상태에 따른 처리
근본 원인 : 유연한 테스트
  • 환경에 대한 제어 능력이 부족할때 발생
미치는 영향 : 유연한 테스트
  • 테스트가 이해하기 어려워 유지 보수가 힘들다.
  • 어떤 테스트 시나리오가 실행됐는지, 모든 시나리오가 전부 실행됐는지 여부를 알 수 없다.
해결책 : 유연한 테스트
  • 유연한 테스트가 필요하게 만드는 의존을 테스트 개발자가 SUT와 분리시키면 된다.
  • 의존을 테스트 스텁이나 모의 객체 같은 테스트 대역으로 교체

원인 : 검증 조건문 로직

  • 테스트 내 조건문 로직을 기대 결과 검증에 사용 할 때 발생
해결책 : 검증 조건문 로직
  • 보호 단언문 사용
    • 실행시키고 싶지 않은 코드에 도달하기 전에 테스트 실패 시킬 때
  • 동등 단언문 사용
    • 제품 코드의 동등 메소드가 엄격 할 때
  • 맞춤 단언문 사용
    • 검증 로직의 반복문 사용 할 때

원인 : 테스트 내 제품 로직

증상 : 테스트 내 제품 로직
  • 테스트의 결과 검증부터 테스트 내 조건문 로직이 있을 때 발생
근본 원인 : 테스트 내 제품 로직
  • 여러 테스트 조건을 단일 테스트 메소드에서 검증하려다 생긴다.
  • 테스트 코드 안에 기대 SUT 로직을 복제 하게 되는 경우
    • ex)여러 입력 인자를 조합해 SUT에 보내 각 입력 값에 대한 기대 결과 값을 열거
해결책 : 테스트 내 제품 로직
  • SUT로 테스트하려는 값들을 미리 계산해 열거해둔다.

원인 : 복잡한 해체

증상 : 복잡한 해체
  • 픽스처 해체 코드가 복잡하면 데이터 누수(data leaks)를 만들어 다른 테스트도 실패하게 만들기 쉽다.
근본 원인 : 복잡한 해체
  • 가비지 컬렉션이 해결해 줄 수 없는 지속적인 자원을 많이 사용할 때 발생
    • ex) static
해결책 : 복잡한 해체
  • 암묵적 해체로 코드를 재사용 가능하게 만든다.
  • 자동 해체 사용
  • 신선한 픽스처나 테스트 대역 사용
    • 픽스처 객체를 해체할 필요가 없으므로..

원인 : 여러 테스트 조건

증상 : 여러 테스트 조건
  • 여러 입력 값과 그에 해당하는 기대 결과 값에 같은 테스트 로직을 적용하려 하는 테스트
근본 원인 : 여러 테스트 조건
  • 하나의 테스트 메소드에서 같은 테스트 로직으로 여러 테스트 조건을 테스트 할 때 발생
해결책 : 여러 테스트 조건
  • 가장 무해한 편 - 다중 테스트 조건
  • 가독성 향상
    • 메소드 뽑아내기
  • 결함 국소화
    • 별개의 테스트 메소드를 만들고 거기에서 인자를 받는 테스트를 호출하게 함
  • 값이 많아진다면 데이터 주도 테스트

테스트하기 힘든 코드

  • 코드가 테스트하기 어렵다.
  • 자동 테스트의 효율성을 높이지 못하게 하는 요소

증상 : 테스트하기 힘든 코드

  • GUI 컴포넌트, 다중스레드 코드, 테스트 코드 등
    • ex) private, 인자가 많은 메소드, 의존성 강한 SUT

미치는 영향 : 테스트하기 힘든 코드

  • 코드의 품질을 쉽게 검증할 수 없게 된다.
  • 품질 평가를 쉽게 반복하기 어렵다.

해결책 패턴 : 테스트하기 힘든 코드

  • 코드를 테스트할 수 있게 만드는 것

원인 : 테스트하기 힘든 코드

  • 밑에서 알아보자!

원인 : 강하게 결합된 코드(하드 코딩된 의존)

증상 : 강하게 결합된 코드
  • 다른 클래스와 함께 테스트하지 않고서는 테스트할 수 없는 클래스
미치는 영향 : 강하게 결합된 코드
  • 따로 실행할 수 없어 단위 테스트하기가 어렵다.
근본 원인 : 강하게 결합된 코드
  • 설계가 조잡하거나, 객체지향 설계 경험이 부족한 경우 발생
해결책 : 강하게 결합된 코드
  • 결합을 분리하면 됨
    • 테스트 주도 개발을 한다면 자연스럽게 된다.

원인 : 비동기 코드

증상 : 비동기 코드
  • (스레드, 프로세스, 프로그램 같은) 실행기를 시작시킨 후 실행기와 상호작용할 수 있을 때까지 기다려야 한다.
미치는 영향 : 비동기 코드
  • 테스트에서 코드와 관련된 SUT 실행을 조절해줘야 하므로 테스트하기 어렵다.
    • 테스트가 복잡해지고 실행이 느려진다.
  • 단위 테스트가 느리면 자주 실행하지 않게 된다.
근본 원인 : 비동기 코드
  • 액티브 객체와 강하게 결합돼 있다.
해결책 : 비동기 코드
  • 로직과 비동기 접근 메커니즘 분리를 해야 한다.
  • 대강 만든 객체로 동기화 방식으로 테스트 할 수 있다.

원인 : 테스트할 수 없는 테스트 코드

증상 : 테스트할 수 없는 테스트 코드
  • 테스트 메소드의 내용이 복잡하거나 테스트 내 조건문 로직이 들어있는 경우 발생
미치는 영향 : 테스트할 수 없는 테스트 코드
  • 버그투성이 테스트가 될 수 있다.
  • 높은 테스트 유지 비용 발생
근본 원인 : 테스트할 수 없는 테스트 코드
  • 테스트 메소드 코드는 자체 검사 테스트로 테스트하기가 어렵다.
해결책 : 테스트할 수 없는 테스트 코드
  • 테스트 메소드를 단순하게 만들어 테스트할 필요가 없게 만든다.
  • 테스트 내 조건문 로직은 테스트 유틸리티 메소드로 바꾼다.

테스트 코드 중복

  • 여러 번 반복되는 테스트 코드

증상 : 테스트 코드 중복

  • 비슷한 작업을 하는 테스트가 여러 개 필요하면 테스트 코드 중복이 생긴다.
  • 테스트케이스 클래스의 테스트 메소드 여기저기에 중복이 될 때는 찾기가 어렵다.

미치는 영향 : 테스트 코드 중복

  • 복사하고 붙여넣기 때문에 발생
  • 높은 테스트 유지 비용 발생

원인 : 테스트 코드 중복

원인 : 잘라 붙여넣기 코드 재사용

  • 코드를 빠르게 작성하는 방법이지만 유지 보수해줘야 하는 코드 복사본이 늘어난다.
근본 원인 : 잘라 붙여넣기 코드 재사용
  • 로직을 재사용하는 기본적인 방법
  • 상세한 코드로부터 큰 그림을 그리는 데 피룡한 리팩토링 기술, 경험이 부족할 때 발생
  • 일정에 대한 압박이 있을 때 발생
해결책 : 잘라 붙여넣기 코드 재사용
  • 메소드 뽑아내기
    • 테스트 유틸리티 메소드로 만든다.
  • 픽스처 설치 로직에 코드 중복
    • 생성 메소드나 찾기 메소드를 사용
  • 결과 검증부에 코드 중복
    • 맞춤 단언문이나 검증 메소드 사용
  • 밖에서 안으로 테스트 작성

원인 : 바퀴 재발명하기

근본 원인 : 바퀴 재발명하기
  • 어떤 테스트 유틸리티 메소드가 있는지 잘 몰라서 발생
해결책 : 바퀴 재발명하기
  • 어떤 테스트 유틸리티 메소드들이 있는지 먼저 확인해봐야 한다.

제품 코드 내 테스트 로직

  • 제품 코드에 테스트에서만 실행돼야 하는 코드가 들어있다.

증상 : 제품 코드 내 테스트 로직

  • 테스트에서만 필요한 로직이 SUT 안에 들어 있을 경우

미치는 영향 : 제품 코드 내 테스트 로직

  • 더 복잡해지고, 추가 버그가 생길 수 있다.

원인 : 제품 코드 내 테스트 로직

원인 : 테스트 훅

  • SUT 내에서 ‘실제’ 코드가 동작할지, 테스트 로직이 동작할지 결정하는 조건문
증상 : 테스트 훅
  • 제품 코드에 테스트용 조건문 생성
미치는 영향 : 테스트 훅
  • 뜻하지 않게 제품 상태에서 실행돼 심각한 문제가 생길 수 있다.
근본 원인 : 테스트 훅
  • 제품 코드 내 테스트 로직 추가 시 발생
해결책 : 테스트 훅
  • 바꿀 수 있는 의존으로 옮긴다.
  • 스트레티지 객체(Strategy Object) 사용
    • 제품 상태에서 기본으로 설치하고 테스트 할 때는 널 객체로 바꾼다.
  • 오버라이딩 가능한 메소드 안에 들어 있다면 테스트용 하위클래스 사용

원인 : 테스트 전용

  • SUT에 들어 있는 테스트만을 위한 코드
증상 : 테스트 전용
  • private 이어야 하는 속성이 public 으로 되어 있는 경우
미치는 영향 : 테스트 전용
  • 소프트웨어 인터페이스의 잠재 고객을 혼란스럽게 만든다.
근본 원인 : 테스트 전용
  • 테스트 주도 개발 시 고객에게 필요 없는 메소드를 추가로 만들게 된다.
  • SUT를 비대칭적으로 사용할 때에도 발생
해결책 : 테스트 전용
  • SUT 테스트용 하위클래스 객체 생성
    • 테스트에서 private한 정보에 접근할 수 있게 한다.
  • 위와 같은 방법 사용 할 수 없다면 테스트 전용 메소드로 만든다.
    • FTO_로 시작하게 만든다.
      • For Tests Only

원인 : 제품 코드 내 테스트 의존

  • 제품 실행이 테스트 실행에 의존
증상 : 제품 코드 내 테스트 의존
  • 특정 테스트 코드가 빌드에 들어 있어야만 제품 코드를 컴파일 or 실행 가능한 경우
미치는 영향 : 제품 코드 내 테스트 의존
  • 테스트 코드가 사용되지 않음에도 실행 크기가 늘어난다.
  • 뜻하지 않게 실행 될 여지가 있다.
근본 원인 : 제품 코드 내 테스트 의존
  • 모듈 간 의존을 신경 쓰지 않을 때 발생
해결책 : 제품 코드 내 테스트 의존
  • 의존 관계를 신중하게 관리해야 함

원인 : 동등 오염

  • SUT의 equals 메소드에 테스트용 동등 구현 시 발생
증상 : 동등 오염
  • 테스트에서의 필요로 인해 equals 메소드를 변경 할 경우
미치는 영향 : 동등 오염
  • 비즈니스 요구를 만족시키지 못할 수 있다.
  • 동등 오염으로 인해 새로운 요구 사항을 지원하는 equals 로직을 추가하기 어려울 수 있다.
근본 원인 : 동등 오염
  • 테스트용 동등의 개념을 잘 모를때 발생
해결책 : 동등 오염
  • 맞춤 단언문으로 내장 동등 단언문을 쓸 수 있게 한다.
  • 비교자(Comparator) 사용
  • 테스트용 하위클래스에서 equals 메소드 구현