///
Search
Duplicate
📘

[요약] 단위 테스트

Updated: 22.09.20
총평
Table of Contents

Part I. 더 큰 그림

1. 단위 테스트의 목표

항상 최대한의 이득을 얻도록 노력해야 한다.
테스트에 드는 노력을 가능한 한 줄이고 그에 따르는 이득을 최대화해야 한다.
코드는 점점 나빠지는 경향이 있다.
변경이 생길 때마다 무질서도가 높아진다.
지속적인 관리가 필요한데 이 때 테스트가 필수적이다.
단위 테스트는 프로젝트의 지속 가능한 성장을 가능하게 하는데 그 목표가 있다.
회귀(regression)
특정 사건 후에 기능이 의도한 대로 작동하지 않는 경우
테스트의 비용
제품 코드를 리팩토링할 때 테스트도 리팩토링한다.
변경 시 테스트를 수행한다. (watch)
테스트를 읽는데 시간을 투자하라
커버리지
코드 커버리지(테스트 커버리지) = 실행 코드 라인 수 / 전체 라인 수
분기 커버리지 = 통과 분기 / 전체 분기
커버리지는 숫자가 매우 낮으면 문제지만 (60%) 이 수치를 넘어 점수가 높다고 해서 다른 의미를 갖진 않는다. (좋은 것도 아님)
성공적인 테스트 스위트란?
테스트를 하나씩 따로 평가하는 방법 뿐.
자동으로 확인할 수 없다.
개발 주기에 통합되어있으며,
가장 중요한 부분만을 대상으로 하고
비즈니스 로직(도메인 모델)이 있는 부분
최소한의 유지비로 최대의 가치를 끌어내는 것.

2. 단위 테스트란?

정의
작은 코드 조각을 검증하고(?)
빠르게 수행하고
격리된 방식으로 처리하는
자동화된 테스트
Mock의 장점 (테스트 대역)
입지상: 동작을 외부 영향과 확실하게 분리하여 테스트 대상에만 집중할 수 있다.
어느 부분이 고장났는지 확실하게 알 수 있다.
Mock의 단점
fragile하다.
구현에 결합되어있다.
공유 의존성
서로의 실행 흐름, 결과에 영향을 미칠 수 있는 수단을 제공하는 의존성
정적 가변 필드
데이터베이스
테스트는 코드의 단위를 검증해서는 안 된다. 동작의 단위를 기준으로 검증해야 한다.
재정의
작은 코드 조각을 검증하고(?) → 기능을 검증하고

3. 단위 테스트 구조

AAA
준비 (Arrange, Given)
실행 (Act, When)
검증 (Assert, Then)
테스트 케이스만 보고 어떤 테스트인지 알 수 있어야 한다.
불확실성을 두지 말 것
명명할 때는 규칙을 두지 말고 동작을 자유롭게 서술할 수 있도록 한다.
마치 비개발자들에게 이 동작(시나리오)을 설명하듯이
테스트 명명 안티패턴
상황, 기댓값 등을 포함한다.
동작 대신 코드를 목표로 하면 해당 코드의 세부 구현에 결합된다.
should be를 사용한다.
사실을 서술할 때는 소망이나 욕구가 들어가지 않아야 한다.
유틸리티 코드의 경우는 함수 이름, 기댓값으로 명명해도 좋다.

Part II. 개발자에게 도움이 되는 테스트 만들기

4. 좋은 단위 테스트의 4대 요소

1.
리팩터링 내성 (Very important)
테스트를 바꾸지 않고 기본 애플리케이션 코드를 리팩터링할 수 있는가
거짓 양성: 기능은 잘 동작하는데 테스트가 깨지는 경우
테스트가 버그 있음을 얼마나 잘 드러내는가
테스트 대상과 구현의 결합도가 높은 경우
최종 결과를 목표로 테스트를 작성해야 한다.
이것은 허위 경보이다. (소음)
2.
빠른 피드백
코드의 변경마다 실행해야 하기 때문에 통과/실패에 대한 피드백에 즉각적으로 전달되어야 한다.
오래 걸리는 테스트는 자주 실행하지 못하고 이는 잘못된 방향으로 코드가 작성되기 쉬우며 더 많은 리소스를 낭비하게 된다.
3.
회귀 방지
테스트가 가능한 한 많은 코드를 실행하는 것을 목표로 해야 한다.
거짓 음성: 기능이 동작하지 않는데 테스트는 통과하는 경우
테스트가 버그 없음을 얼마나 잘 드러내는가
이것은 구현이 잘못됐다는 신호이다.
4.
유지보수성
테스트가 얼마나 이해하기 어려운가
테스트가 얼마나 실행하기 어려운가
사전에 띄워야만 하는 무언가가 있다면 어려운 것 (데이터베이스)
테스트 정확도 = 신호(발견된 버그 수) / 소음(허위 경보 발생 수)
이상적인 테스트?
위 4대 요소는 곱셈으로 계산되며 어떤 특성이라도 0이 되면 전체가 0이 된다.
테스트가 가치 있으려면 네 가지 모두 점수를 내야 한다.
1,2,3은 상호 배타적이다.
두 가지를 선택해서 극대화하고 한 가지를 희생해야 한다.
우선 “리팩터링 내성"을 최대한 많이 갖는 것을 목표로 해야 한다.
리팩터링 내성은 0 또는 1이기 때문이다.
그 다음 얼마나 버그를 잘 찾아내는지, 얼마나 빠른지를 절충하게 된다.
가치없는 테스트 1. E2E테스트
회귀방지도 훌륭하고 리팩터링 내성도 우수하지만 느린 속도가 문제이다.
모든 의존성을 함께 관리해줘야 하기 때문에 유지보수 비용도 많이 들어가는 단점이 있다.
가치없는 테스트 2. 간단한 테스트
고장이 없을 것 같은 단순 getter, setter 코드를 테스트하는 것은 회귀 방지에 의미가 없다.
가치없는 테스트 3. 깨지기 쉬운 테스트
거짓 양성이 많은 테스트를 Brittle test(깨지기 쉬운 테스트)라고 한다.
블랙 박스 테스트 vs 화이트박스 테스트
블랙박스
내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 방법
화이트박스
내부 작업을 검증하는 테스트 방식. 명세가 아닌 소스 코드에서 파생된다.
블랙 박스를 기본으로 선택해야 한다.
테스트를 통해 비즈니스 요구 사항으로 거슬러 올라갈 수 없다면 이는 테스트가 깨지기 쉬움을 나타낸다.
코드 내부 구조에 대해 전혀 모르는 것처럼 테스트 하라.
검증문을 작성할 때 제품 코드에 의존하지 말라.
테스트에서 별도의 리터럴과 상수 집합을 사용하라.
독립적으로 검사점을 제공해야 한다.

5. 목과 테스트의 취약성

테스트 대역엔 목과 스텁 두 종류가 있다.
목 (mock, spy)
외부로 나가는 상호 작용을 모방
상태를 변경하기 위한 의존성을 호출하는 것
ex. 이메일 발송
스텁 (stub, dummy, fake)
내부로 들어오는 상호 작용을 모방
입력 데이터를 얻기 위한 의존성을 호출하는 것
ex. 데이터베이스로부터 데이터 검색
과잉 명세 (overspecification)
최종 결과가 아닌 사항을 검증하려는 관행
스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티 패턴
CQS (Command Query Separation)
명령(Command): 부작용을 일으키고 어떤 값도 반환하지 않는 메서드 (return void)
mock으로 대체
조회(Query): 부작용이 없고 값을 반환하는 것,
stub으로 대체
테스트는 어떻게가 아니라 무엇에 중점을 둬야 한다.
무엇, 공개 API, 식별할 수 있는 동작
연산: 계산을 수행하거나 부작용을 초래하는 메서드
하나의 기능에 하나의 연산이어야 한다. 하나의 기능을 위해 여러 연산을 사용해야 한다면 응집도가 떨어지는 구현인 것이다.
상태: 시스템의 현재 상태
API를 잘 설계하면 단위 테스트도 자동으로 좋아진다.
시스템 내 통신에서 목을 사용하면 취약한 테스트로 이어진다.
통신의 부작용이 외부 환경에서 보일 때만 목을 사용해야 한다.

6. 단위 테스트 스타일

출력 기반 테스트 (output based testing)
순수 함수 방식으로 작성된 코드에만 적용
반환 값만 검증하면 된다.
거짓 양성 방지가 우수하다.
상태 기반 스타일 (state based testing)
작업이 완료된 후 시스템 상태를 확인하는 것
통신 기반 스타일 (communication based testing)
협력자 간의 통신을 검증하는 것
허위 경보에 가장 취약하다.
*코드 오염 (code pollution)
단지 단위 테스트를 가능하게 하거나 단순화하기 위한 목적만으로 제품 코드베이스를 오염시키는 것
출력 기반 >>> 상태기반 >>>>>>>>>> 통신(상호작용, 커뮤니케이션) 기반

7. 가치 있는 단위 테스트를 위한 리팩터링

코드는 깊거나 넓거나 둘 중 하나여야 한다. 둘 다 일 수는 없다. 둘 다인 코드는 깊은 코드와 넓은 코드로 분리해야 한다.
깊다: 복잡도 또는 도메인 유의성
넓다: 협력자 수
험블 객체 패턴
1.
암시적 의존성을 명시적으로 만든다.
2.
서비스 계층을 도입하여 도메인 모델이 외부 시스템과 직접 통신하는 문제를 해결한다.
3.
서비스의 복잡도를 낮춘다.
a.
도메인 모델을 인스턴스화 → 팩토리 클래스
4.
하나의 관심사에만 집중할 수 있도록 도메인 모델을 분리하고 새로 생성한다.
CanDo/Do 패턴
도메인 이벤트 패턴

Part III. 통합테스트

8. 통합 테스트를 하는 이유

단위 테스트와 일정 비율을 맞춰야 한다.
1:1 이 최대이며 프로젝트가 커질수록 단위테스트의 비중이 커진다.
통합 테스트는 유지 비용이 많이 들어가기 때문이다.
프로세스 외부 의존성 운영이 추가로 필요하고
관련된 모듈들(협력자)이 많아서 테스트가 비대해지기 때문이다.
통합 테스트는 주요 흐름 happy path과 단위 테스트가 다루지 못하는 edge case를 주로 다룬다.
단위 테스트에서는 기본적인 비즈니스 요구 사항과 예외 사항들.
프로세스 외부 의존성
관리 의존성 (전체를 제어할 수 있는) - 데이터베이스
실제 인스턴스를 사용한다.
비관리 의존성 (전체를 제어할 수 없는) - SMTP 서버
목으로 대체한다.

9. 목 처리에 대한 모범 사례

목은 통합 테스트만을 위한 것
비관리 의존성과의 통신에선 다음 두 가지를 확인한다.
예상하는 호출이 있는가?
예상치 못한 호출은 없는가?

Part IV. 단위 테스트 안티 패턴

비공개 메서드 단위 테스트
하지 않아야 한다.
비공개 메서드가 엄청 복잡해서 공개 API로 테스트가 불가능하다면?
죽은 코드거나
추상화가 덜 되어 있는 것이다.
간접 테스트한다.
비공개 상태 노출
단위 테스트 목적으로 상태를 노출하는 경우
테스트로 유출된 도메인 지식
sum 을 테스트 하는데, [1, 2] 를 전달하고 기대값으로 1+2 를 하는 것이다. 이는 구현이 노출된 것이다.
복잡한 예로 보면 구현을 그대로 복붙하게 되는 것이다.
코드 오염
테스트에만 필요한 제품 코드를 추가하는 것
제품 코드와 테스트 코드가 혼재되면 유지비용이 증가하게 된다.
둘은 분리해야 한다.
시간 관리 하다가 앰비언트 컨텍스트 패턴 (ambient context)를 사용
프레임워크 내장 시간 API를 바로 사용하는 대신 그것을 함수로 만들고 함수로 주입받는 방식
명시적 의존성으로서의 시간
일반 값으로 주입할 수 있도록 하자.
끝.

마무리