[xUnit 테스트 패턴] 16장 - 동작 냄새


16장 - 동작 냄새

단언 룰렛

  • 한 테스트 메소드 안에 있는 여러 단언문 중 어디에서 테스트 실패가 생겼는지 알기 어렵다.

증상 : 단언 룰렛

  • 정확하게 어느 단언문에서 실패했는지 알 수 없다.

미치는 영향 : 단언 룰렛

  • 어느 단언문에서 실패했는지 알 수 없기 때문에 문제 수정이 어렵고 시간도 많이 걸린다.

원인 : 단언 룰렛

원인 : 욕심쟁이 테스트

  • 한번에 너무 많은 기능을 검증하는 테스트
증상 : 욕심쟁이 테스트
  • 픽스처 설치 로직과 단언문 여기저기에서 같은 메소드를 여러 번 호출할 때 발생
  • 개발자가 테스트 자동 프레임워크를 수정하는 것도 증상 중 하나
근본 원인 : 욕심쟁이 테스트
  • 하나의 테스트 메소드에 여러 테스트 조건을 검증해 단위 테스트의 숫자를 줄이려고 할 때 나타남
해결책 : 욕심쟁이 테스트
  • 욕심쟁이 테스트를 쪼개어 단일 조건 테스트 스위트에 넣는다.
  • 메소드 뽑아내기 리팩토링 진행
  • 고객 테스트에서 복잡한 픽스처 설치가 필요하다면 뒷문 설치로 픽스처를 테스트의 초반부와 후반부용으로 설치해서 테스트를 두 부분으로 나눈다.

원인 : 빠진 단언 메시지

증상 : 빠진 단언 메시지
  • 테스트 실행기의 출력을 확인해도 어느 단언문에서 실패했는지 알 수 없다.
근본 원인 : 빠진 단언 메시지
  • 빼먹은 단언 메시지로 단언 메소드를 호출할 때 발생
  • 같은 종류의 단언 메시지가 하나 이상 있을 경우
해결책 : 빠진 단언 메시지
  • IDE 통합 그래픽 테스트 실행기에서 실행
  • 단언 메시지를 모든 단언 메소드 호출에 추가

변덕스러운 테스트

  • 어떨 때는 통과하고 어떨 때는 실패하는 테스트

증상 : 변덕스러운 테스트

  • 테스트를 언제, 누가 실행했느냐에 따라 다른 결과가 나온다.

미치는 영향 : 변덕스러운 테스트

  • 변덕스러운 테스트를 테스트 스위트에 그대로 두면 다른 문제까지도 이미 알고 있는 실패라고 생각해 숨겨질 수 있다.

문제 해결을 위한 조언 : 변덕스러운 테스트

  • 정보를 모아야 함
    • 어디에서 테스트가 성공하고 실패하는지
    • 모든 테스트가 실행되는지, 일부만 실행되는지
    • 테스트 스위트를 연달아 여러 번 실행 했을 때 어떤 변화가 있는지
    • 동시에 테스트를 실행할 때 어떤 변화가 있는지

원인 : 서로 반응하는 테스트

  • 테스트가 다른 테스트에 의존하고 있다.
증상 : 서로 반응하는 테스트
  • 잘 돌아가는 테스트가 갑자기 실패 할 때
    • 스위트에 다른 테스트를 추가 or 삭제 시
    • 같은 스위트의 다른 테스트가 실패 or 성공
    • 해당 테스트의 이름을 바꾸거나 소스 파일 위치를 바꿨을 때
    • 새로운 버전의 테스트 실행기를 설치 했을 때
근본 원인 : 서로 반응하는 테스트
  • 공유 픽스처를 쓰면서 다른 테스트의 결과에 의존할 때 서로 반응하는 테스트가 생김
  • 서로 반응하는 메커니즘
    • static 변수에 의존
      • 테스트 시작 시마다 초기화 해줘야 한다.
  • 서로 반응하는 이유
    • 다른 테스트의 픽스처 설치 단계에서 생성한 픽스처에 의존
    • 다른 테스트의 SUT 실행 단계에서 변경한 SUT에 의존
    • 상호 배타적인 실행으로 인해 생긴 충돌
  • 의존 테스트가 의존 관계 사라질 때
    • 스위트에서 제거
    • SUT의 상태를 변경하지 않게 수정
    • SUT의 상태를 변경하려는 시도가 실패
    • 문제가 되는 테스트 보다 뒤에 실행
  • 충돌하기 시작 할 때
    • 스위트에 추가
    • 가장 먼저 실행
    • 의존하고 있는 테스트보다 먼저 실행
해결책 : 서로 반응하는 테스트
  • 신선한 픽스처 사용
  • 공유 픽스처 사용 -> 불변 공유 픽스처로 변경
  • 지연 설치
  • 픽스처 설치 코드를 생성 메소드에 둔다.
  • 테스트 도우미
  • 자동 픽스처 해체

원인 : 서로 반응하는 테스트 스위트

  • 서로 다른 테스트 스위트에 들어 있음
증상 : 서로 반응하는 테스트 스위트
  • 스위트들의 스위트에서 실행할 때 실패함
근본 원인 : 서로 반응하는 테스트 스위트
  • 별개의 테스트 스위트에 있는 테스트에서 같은 자원을 생성하려고 할 때 발생
  • 공유 픽스처와 관련된 것들 살펴봐야 함
    • 테스트 메소드 안쪽 or setUp 메서도 or 테스트 유틸리티 메소드
해결책 : 서로 반응하는 테스트 스위트
  • 신선한 픽스처 사용
  • 공유 픽스처 사용 -> 불변 공유 픽스처로 변경
  • 자동 해체

원인 : 외로운 테스트

  • 스위트의 일부로는 실행할 수 있지만 단독으로는 실행 할 수 없는 테스트
  • 다른 테스트에서 생성한 공유 픽스처나 픽스처 설치 로직(설치 데코레이터)에 의존하기 때문

원인 : 자원 누수

  • 테스트나 SUT에서 한정된 자원을 써 버린다.
증상 : 자원 누수
  • 테스트가 점점 느려지다가 어느 순간 실패한다.
근본 원인 : 자원 누수
  • 테스트나 SUT에서 한정된 자원을 할당한 후 나중에 해제하지 못해 자원을 소모
    • SUT에서 자원을 적절하게 정리하지 못함
    • 테스트에서 픽스처 설치 시 할당한 자원을 픽스처 해체 시 정리하지 못함
해결책 : 자원 누수
  • SUT 문제이면 SUT의 버그를 고치면 된다.
  • 테스트 문제이면 확정 인라인 해체를 쓰거나 자동 해체 써야 한다.

원인 : 자원 낙관주의

  • 외부 자원에 의존하는 테스트가 실행 되는 거에 따라 결과가 비결정적
증상 : 자원 낙관주의
  • 테스트가 환경에 따라 성공 or 실패
근본 원인 : 자원 낙관주의
  • 어떤 환경에서는 쓸 수 있는 자원을 다른 환경에서는 쓸 수 없다.
해결책 : 자원 낙관주의
  • 신선한 픽스처 사용
  • 외부 자원 사용 시 소스코드 저장소(SCM)에 저장해 모든 테스트 실행기가 같은 환경에서 실행 될 수 있게 해야 한다.

원인 : 반복 안 되는 테스트

  • 이전에 실행됐을 때의 자기 자신과 상호작용 한다.
증상 : 반복 안 되는 테스트
  • 테스트가 처음에는 성공하고 다음부터는 실패 또는 반대로 실행
근본 원인 : 반복 안 되는 테스트
  • 미리 만든 픽스처에서 일반적으로 발생
  • 데이터베이스 샌드박스를 써도 내가 실행한 테스트끼리 충돌이나 같은 테스트 실행기에서 실행 중인 다른 테스트에서 생기는 충돌은 막을 수 없다.
  • 지연 설치로 클래스 변수에 픽스처를 설치하면 테스트 픽스처 재초기화 안됨
    • 테스트 실행기에서 실행된 모든 테스트는 같은 테스트 픽스처 공유
해결책 : 반복 안 되는 테스트
  • 신선한 픽스처 사용
  • 개별 테스트가 끝난 후 데이터베이스 샌드박스 같은 모든 종류의 공유 픽스처 전부 제거 해야 함
    • 데이터베이스를 가짜 데이터베이스로 바꾼다.
  • 자동 해체로 모든 객체와 로우를 제거

원인 : 테스트 실행 전쟁

  • 여러 사람이 동시에 테스트를 실행하면 테스트가 무작위로 실패한다.
증상 : 테스트 실행 전쟁
  • 데이터베이스 같은 외부 공유 자원에 의존하는 테스트 실행 시 발생
근본 원인 : 테스트 실행 전쟁
  • 전역 공유 픽스처가 있을때에만 발생
    • 파일이나 테스트 데이터베이스의 레코드
  • 데이터베이스 경쟁
    • DB Lock
해결책 : 테스트 실행 전쟁
  • 신선한 픽스처 사용
  • 테스트 실행기마다 별도의 데이터베이스 샌드박스 생성

원인 : 비결정적 테스트

  • 하나의 테스트 실행기에서만 테스트 실행했는데도 테스트가 무작위로 실패한다.
증상 : 비결정적 테스트
  • 테스트 실행시키면 매번 결과가 다르다.
미치는 영향 : 비결정적 테스트
  • 디버깅이 오래 걸림
  • 실패 재현 어려움
근본 원인 : 비결정적 테스트
  • 테스트가 실행될 때마다 다른 값을 쓸 때 발생 할 수 있다.
  • ex) 시스템에서 음수를 다르게 취급하거나 최대 허용 값이 있을 때 숫자를 임의로 생성해 사용될 때
  • ex) 문자열 값에 최대 허용 길이 제한이 있는 경우 별도 분자형으로 변환해 사용 할 때
해결책 : 비결정적 테스트
  1. 테스트 내 조건문 로직을 모두 제거해 테스트를 한 방향으로만 실행
  2. 모든 무작위 값을 결정적인 값으로 변경

깨지시 쉬운 테스트

증상 : 깨지시 쉬운 테스트

  • 잘 돌아가던 테스트가 컴파일이 안 되거나 테스트 실패

미치는 영향 : 깨지시 쉬운 테스트

  • 테스트 유지 비용이 늘어남

원인 : 깨지시 쉬운 테스트

  • 간접 테스팅의 신호
  • 욕심쟁이 테스트의 신호
  • 서로 강하게 결합되어 있음
  • 4가지 민감함 중 하나의 형태로 나타남

원인 : 인터페이스에 민감함

  • SUT의 인터페이스 일부가 바뀜으로 인해 테스트가 컴파일이 안되거나 실패
증상 : 인터페이스에 민감함
  • API 호출 할 때 테스트 에러 발생
  • SUT와 상호작용하는데 필요한 사용자 인터페이스 요소를 찾지 못해 실패
해결책 : 인터페이스에 민감함
  • SUT API 캡슐화
    • SUT API를 테스트로부터 숨겨주는 테스트 유틸리티 메소드의 형태로 구현 할 수 있다.
  • 모든 변경은 하위 호환성을 지원해야 한다.

원인 : 동작에 민감함

  • SUT를 변경했을 때 다른 테스트가 실패한다면 동작에 민감함이 있는 것
증상 : 동작에 민감함
  • SUT에 새로운 기능을 추가하거나 버그를 고친 이후로 잘 돌아가던 테스트가 실패한다.
근본 원인 : 동작에 민감함
  • 회귀 테스트에서 SUT의 테스트 전 상태를 설치하는 기능이 변경 될 때
  • 회귀 테스트에서 SUT의 테스트 전 상태를 검증하는 기능이 변경 될 때
  • 회귀 테스트에서 픽스처 해체 코드가 변경 될 때
해결책 : 동작에 민감함
  • 생성 메소드 안으로 캡슐화
  • 맞춤 단언문
  • 검증 메소드 안에 캡슐화

원인 : 데이터에 민감함

  • 데이터가 변경됐을 때 테스트가 실패함.
증상 : 데이터에 민감함
  • 테스트 실패 케이스
    • 데이터 추가
    • 레코드 변경 또느 삭제
    • 공유 픽스처를 설치하는 코드를 변경
근본 원인 : 데이터에 민감함
  • SUT에서 사용하는 입력 값이 참조하는 데이터가 없을 경우
  • 데이터베이스에 없는 데이터를 찾는 경우
해결책 : 데이터에 민감함
  • 최고의 방법은 테스트를 데이터베이스의 데이터와 독립적으로 만드는 것
  • 위의 방법이 불가능하면 데이터베이스 분할 스키마 사용
  • 델타 단언문 사용
    • 데이터의 이전, 이후 ‘스냅샷’을 비교해 변경되지 않은 데이터는 무시

원인 : 문맥에 민감함

  • SUT가 실행되는 문맥의 상태나 동작이 변경돼 테스트가 실패하는 경우
근본 원인 : 문맥에 민감함
  • 검증하려는 기능이 시간이나 날짜에 의존 관계가 있다.
  • SUT에서 의존하는 코드나 시스템의 동작이 변경됐다.

원인 : 심하게 명세된 소프트웨어

  • 테스트에서 소프트웨어의 목적이 아니라 과정을 설명하는 경우
  • 테스트에서 SUT의 구현에 대해 너무 많이 알아야 하는 경우
    • 정문을 먼저 사용 원칙으로 피할 수 있다.

원인 : 민감한 동등

  • 검증하려는 객체를 문자열로 바꿔 기대 문자열과 비교하는 경우
    • 비즈니스와는 상관없는 동작에 민감함

원인 : 깨지시 쉬운 픽스처

  • 표준 픽스처를 새로 추가된 테스트 때문에 변경 시 여러 개의 다른 테스트가 실패하는 경우

잦은 디버깅

  • 테스트 실패 원인을 찾으려면 직접 디버깅을 해야 한다.

증상 : 잦은 디버깅

  • 테스트가 실패할 때마다 디버깅 해야 하는 경우

원인 : 잦은 디버깅

  • 자동 테스트 스위트에 결함 국소화가 부족할 때 발생
    • 각 클래스에서 생긴 논리 에러를 알려줄 수 있는 상세한 단위 테스트가 없다.
    • 클래스 묶음(컴포넌트) 용 테스트가 없다.
  • 드문 테스트 실행으로도 발생 할 수 있다.

미치는 영향 : 잦은 디버깅

  • 생산성 저하 및 개발 일정 늘어남

해결 패턴 : 잦은 디버깅

  • 테스트 주도 개발을 해라!

수동 조정

  • 실행할 때마다 수작업을 해줘야 하는 테스트

증상 : 수동 조정

  • 사람이 직접 테스트 실행기의 결과를 검증

미치는 영향 : 수동 조정

  • 완전 자동 통합 빌드와 회귀 테스트 과정이 불가능하다.

원인 : 수동 픽스처 설치

증상 : 수동 픽스처 설치
  • 서버 설치, 서버 프로세스 실행, 미리 만든 픽스처 설치 스크립트 실행
근본 원인 : 수동 픽스처 설치
  • 픽스처 설치 단계 자동화에 신경 쓰지 않을 때 발생
해결책 : 수동 픽스처 설치
  • 테스트용 API 만들어 테스트에서 픽스처 설치
  • 수동으로 해줘야 하는 부분을 SUT에서 분리하는 리팩토링 진행

원인 : 수동 결과 검증

증상 : 수동 결과 검증
  • SUT가 올바른 결과를 리턴하지 않는다는 걸 알고 있는 데도 테스트가 성공
근본 원인 : 수동 결과 검증
  • 자체 검사 테스트가 아니라면 에러나 예외 발생하기 전까지는 테스트가 성공하므로 잘 돼가고 있다고 착각할 수 있다.
해결책 : 수동 결과 검증
  • 단언 메소드 같은 검증 로직 추가

원인 : 수동 이벤트 발생

증상 : 수동 이벤트 발생
  • 테스트 실행 도중에 수동으로 어떤 작업을 해줘야 한다.
근본 원인 : 수동 이벤트 발생
  • 네트워크 장애
  • 데이터베이스 장애
  • 사용자 인터페이스 이벤트 발생
미치는 영향 : 수동 이벤트 발생
  • 완전 자동 빌드 후 테스트 주기를 만들기 어렵게 함
해결책 : 수동 이벤트 발생
  • 비동기 이벤트
    • 가짜 이벤트 전달
  • 동기적 응답
    • 테스트 스텁으로 변경해 간접 입력 제어

느린 테스트

  • 테스트 실행이 너무 오래 걸린다.

미치는 영향 : 느린 테스트

  • 생상성 저하

문제 해결을 위한 조언 : 느린 테스트

  • 테스트케이스 상위클래스의 setUp, tearDown 메소드를 수정해 테스트의 시작/끝 시각이나 테스트에 걸린 시간을 테스트케이스 클래스, 테스트 메소드 이름과 함께 로그로 남긴다.

원인 : 느린 테스트

  • 레거시 코드나 ‘테스트 나중’ 방식으로 구현한 코드에서 주로 발생

원인 : 느린 컴포넌트 사용

  • SUT의 어떤 컴포넌트 응답이 굉장히 느리다.
근본 원인 : 느린 컴포넌트 사용
  • 여러 테스트에서 데이터베이스 사용 할 때
    • 메모리 데이터 보다 50배는 느리게 실행된다.
해결책 : 느린 컴포넌트 사용
  • 테스트 대역으로 교체
  • 가짜 데이터베이스 사용

원인 : 일반 픽스처

증상 : 일반 픽스처
  • 모든 테스트에서 똑같이 복잡한 픽스처 생성
근본 원인 : 일반 픽스처
  • 모든 테스트에서 매번 신선한 픽스처 형태로 거대한 일반 픽스처 생성
해결책 : 일반 픽스처
  • 일반 픽스처를 공유 픽스처로 구현
  • 테스트마다 수행하는 픽스처 설치 작업량을 줄인다.

원인 : 비동기 테스트

증상 : 비동기 테스트
  • 일부 테스트가 지나치게 느리다.
근본 원인 : 비동기 테스트
  • 테스트 메소드에 지연 코드가 들어 있을 때
    • 스레드나 프로세스 생성하고 검증할 때 까지 기다림..
미치는 영향 : 비동기 테스트
  • 테스트 수행 시간 증가
해결책 : 비동기 테스트
  • 로직을 동기적인 방식으로 테스트
  • 대강 만든 실행기 구현

원인 : 너무 많은 테스트

증상 : 너무 많은 테스트
  • 시간이 많이 걸린다.
근본 원인 : 너무 많은 테스트
  • 테스트가 너무 많은게 원인
해결책 : 너무 많은 테스트
  • 언제나 모든 테스트를 실행 -> 정기적으로 모든 테스트 실행
  • 시스템의 크기가 크다면
    • 독립적인 하위시스템이나 컴포넌트로 나눈다.