테스트 코드 작성 가이드(근데 이제 Jest를 곁들인)
안녕하세요, 비브로스에서 백엔드 개발자로 일하고 있는 김예림입니다.
개발 과정에서 이제는 테스트 코드가 필수처럼 여겨지고 있지만, 여전히 어렵고 멀게 느껴지는 경우가 많습니다. 저 역시 테스트 코드를 작성하면서 내가 쓰고 있는 기법의 정확한 의미나 스타일을 잘 알지 못하거나, 어느 정도로 작성해야 할지 감이 오지 않아 혼란스러웠던 경험이 많았습니다. 이런 경험들이 쌓이다 보니 점점 테스트 코드의 필요성도 희미해지고 귀찮고 불필요하게 느껴질 때도 있었습니다.
그래서 이번 글에서는 저와 비슷한 경험을 하신 분들을 위해 테스트 코드 작성 가이드를 준비해보았습니다. 이 글은 방대한 이론서처럼 어렵게 쓰인 문서가 아니라, 가볍게 읽으며 기초와 목적성을 다질 수 있는 내용을 담았습니다. 글을 읽고 테스트 코드 작성의 감을 잡아보고, 더 궁금한 점이 생기면 스스로 찾아보게 되는 출발점이 되었으면 합니다.
여기에 Jest를 살짝 곁들였으니, 맛있게 읽어주세요! 😊

개발하면서 얼마나 테스트를 하고 있나요? 결과물에 급급하여 테스트 코드를 제대로 작성하지 못한 채 배포한 적이 있나요? 이 글은 그런 상황을 겪는 개발자들 을 위해 테스트 코드의 기본 개념과 방법론에 대해 설명합니다.
단위 테스트 (Unit Test)
단위 테스트는 소프트웨어의 개별 구성 요소(모듈, 함수, 메서드 등)가 의도된 대로 정확히 작동하는지 검증하는 절차입니다. 다시 말해, 특정 코드 조각의 올바른 동작을 보장하기 위해 테스트 케이스를 작성하고 실행하는 과정입니다.
테스트 대상 정의
단위 테스트의 테스트 대상은 테스트 중인 주제 (Subject Under Test, SUT) 라고 불리며, 테스트하려는 주요 객체나 함수를 의미합니다. SUT는 일반적으로 테스트의 중심에 놓이는 핵심 코드입니다.
단위 테스트의 격리
단위 테스트의 핵심 목표는 테스트 대상을 외부 요인으로부터 최대한 격리하는 것입니다. 이를 위해 Mock (모의 객체), Stub (스텁), 또는 Fake 객체와 같은 **테스트 대역(Test Double)**을 사용하여 외부 의존성을 시뮬레이션합니다.

예를 들어, 클래스 A를 단위 테스트할 때, A가 클래스 B와 C가 상호작용한다고 가정해 봅시다. 이 경우, 클래스 B, C를 실제로 호출하지 않고도 A를 테스트할 수 있도록 Mock 객체를 활용해 B, C의 동작을 시뮬레이션합니다. 이를 통해 테스트의 단순성과 독립성을 유지할 수 있습니 다.
단위 테스트의 장단점
장점
- 빠르다: 단위 테스트는 코드의 작은 단위를 테스트하기 때문에 실행 속도가 빠릅니다.
- 간단하다: 테스트 범위가 명확하고 복잡도가 낮아 작성하기 쉽습니다.
- 신뢰성 확보: 개별 모듈의 동작이 보장되므로 코드 품질이 향상됩니다.
- 코드 변경시 회귀 버그 예방: 새로운 기능 추가나 코드 변경시 기존 기능이 깨지지 않도록 보장합니다.
- 유지보수 시 변경 범위를 신속히 파악가능: 테스트 실패를 통해 어떤 코드가 영향을 받는지 바로 확인 할 수 있고 그걸 토대로 변경범위가 파악이 가능합니다.
- 스펙문서 역할: 테스트 코드는 모듈의 입출력, 동작을 명확히 설명하기때문에 신규 인원이 테스트 코드를 읽으면서 모듈의 스펙과 동작을 빠르게 이해할 수 있습니다.
- 리팩터링의 안전망 제공: 테스트 코드가 리팩터링의 안전망 역할을 합니다. 기존 테스트가 성공하면 리팩터링이 성공적으로 이루어진 것을 보장 합니다.
단점
- 현실성이 떨어질 수 있다: 단위 테스트는 개별 모듈의 동작만 검증하므로, 시스템 전체의 복잡한 상호작용을 포괄하지 못할 수 있습니다
- 제약 조건이 많다: 외부 의존성을 제거해야 하기 때문에 설정이 번거로울 수 있습니다.
좋은 단위 테스트란?
좋은 단위 테스트는 다음과 같은 특징이 있습니다.
- 빠르게 실행되어야 한다.
- 테스트 환경을 일관되게 유지하고, 테스트 결과가 항상 예측 가능해야한다.
- 다른 테스트와 완전히 독립적으로 실행되어야한다.
- 시스템 파일, 네트워크, 데이터베이스가 없어도 메모리 내에서 실행되어야한다.
- 가능한 한 동기적인 흐름으로 실행되어야 하며, 불필요한 병렬 스레드를 사용하지 않아야 합니다.
모든 테스트가 좋은 단위 테스트의 특성을 전부 만족하는 것은 사실 불가능에 가깝습니다. 그래서 항상 모든 조건을 만족할 필요는 없습니다. 단위테스트 조건을 만족하기 까다로운 테스트는 적당한 리팩터링을 거쳐 보다 많은 조건을 충족하도록 만들 수도 있지만, 통합 테스트로 만드는 것도 하나의 방법일 수도 있습니다.
통합 테스트 (Integration Test)
통합 테스트는 2 개 이상의 여러 컴포넌트나 모듈이 통합되어 상호작용할 때의 동작을 검증하는 절차입니다. 이를 통해 모듈 간의 인터페이스와 데이터 흐름이 올바르게 작동하는지 확인하는 것이 주 목적입니다.
통합 테스트의 주요 특징
- 모듈 간 상호작용 검증
- 개별적으로 검증된 모듈들이 함께 작동할 때, 데이터가 정확히 전달되고 동작이 예상한 대로 이루어지는지 확인합니다.
- 예: 사용자 인증 서비스와 데이터베이스가 올바르게 연동되어 로그인 요청이 처리되는지 확인.
- 외부 의존성 포함 가능
- 통합 테스트는 내부 시스템뿐 아니라 데이터베이스, 외부 API 등 외부 의존성과의 상호작용도 다룹니다. 이를 통해 외부 서비스와의 연결 상태와 데이터 교환의 정확성을 확인할 수 있습니다.
- 단위 테스트보다 넓은 범위
- 단위 테스트가 개별 모듈의 동작에 집중한다면, 통합 테스트는 그 모듈들이 실제로 서로 조화를 이루며 작동하는지를 보장합니다.
통합 테스트 작성 시 유의점
- 명확한 테스트 범위 설정
- 통합 테스트는 단위 테스트보다 범위가 넓지만, 너무 광범위하면 디버깅이 어려워집니다. 특정 모듈 간의 상호작용에 초점을 맞추는 것이 중요합니다.
- 테스트 환경 구성
- 실제 환경과 유사한 테스트 환경을 구성해야 합니다.
- 데이터베이스나 API와의 통합을 검증하는 경우, 테스트 데이터베이스를 사용하는 것이 일반적입니다.
- Mock 객체나 Stub을 사용해 일부 외부 의존성을 시뮬레이션할 수도 있습니다.
- 속도와 성능 고려
- 통합 테스트는 단위 테스트보다 느리기 때문에 필요한 경우에만 실행하고, CI/CD 파이프라인에 통합하여 자동화합니다.
- 실패 시 디버깅 전략
- 통합 테스트는 범위가 넓기 때문에 실패 원인을 찾기 어려울 수 있습니다. 로그와 디버깅 도구를 활용해 문제를 빠르게 진단할 수 있도록 설계합니다.
통합 테스트의 장단점
장점:
- 모듈 간 상호작용과 데이터 흐름의 실제 문제를 발견할 수 있음.
- 외부 서비스와의 통합을 검증함으로써 시스템 안정성을 높임.
- 사용자 관점에서 발생할 수 있는 다양한 문제를 미리 확인 가능.
단점:
- 단위 테스트보다 느리고 설정이 복잡함.
- 외부 의존성에 따라 테스트 결과가 변할 가능성이 있음(네트워크 장애, 서비스 중단 등).
- 실패 시 원인 분석이 어려울 수 있음.
E2E 테스트 (End-to-End Test)
E2E 테스트는 시스템의 전체 흐름을 사용자 관점에서 검증하는 절차입니다. 애플리케이션의 주요 기능과 사용자의 시나리오가 제대로 동작하는지 확인하는 것이 주 목적입니다.
E2E 테스트의 주요 특징
- 사용자 시나리오 기반 테스트
- E2E 테스트는 실제 사용자가 애플리케이션을 사용하는 방식으로 테스트를 진행합니다.
- 예: 사용자가 로그인 버튼을 클릭하고, 대시보드가 성공적으로 로드되는지 확인.
- 전체 시스템 검증
- E2E 테스트는 프론트엔드, 백엔드, 데이터베이스, 외부 서비스 등 애플리케이션의 모든 구성 요소가 통합된 상태에서 동작을 검증합니다.
- 실제 환경과 유사한 테스트 환경
- 실제 프로덕션 환경과 유사하게 설정하여 시스템의 복잡한 시나리오를 재현합니다.
E2E 테스트 작성 시 유의점
- 테스트 커버리지 설정
- 모든 사용자 시나리오를 테스트하려는 것은 비효율적일 수 있습니다. 핵심 사용 자 흐름에 초점을 맞추는 것이 중요합니다.
- 테스트 환경 관리
- 테스트 실행 시, 프로덕션 데이터나 환경이 변경되지 않도록 주의해야 합니다. 별도의 테스트 전용 환경을 마련합니다.
- 자동화 도구 활용
- Cypress, Selenium, Playwright 같은 E2E 테스트 도구를 사용하여 테스트를 자동화하고, 안정성과 반복성을 확보합니다.
- 실패 분석과 유지보수
- E2E 테스트는 실행 시간이 길고, 실패 원인을 분석하는 데 시간이 걸릴 수 있습니다. 잘 설계된 테스트 로그와 명확한 오류 메시지가 필요 합니다.
E2E 테스트의 장단점
장점:
- 사용자 관점에서 실제 동작을 확인할 수 있어, 사용자 경험에 대한 신뢰성을 확보 가능.
- 전체 시스템을 검증하므로, 통합 테스트에서 놓칠 수 있는 문제를 발견 가능.
단점:
- 실행 시간이 길고 자원 소모가 많음.
- 디버깅이 복잡하며, 작은 변화에도 테스트가 실패할 가능성이 있음.
- 설정 및 유지보수 비용이 높음.
여기서는 단위테스트와 통합테스트 위주로만 설명하도록 하겠습니다
통합 테스트 vs. E2E 테스트
통합 테스트는 우리의 구성요소와 외부 구성요소 간의 상호작용에 중점은 둡니다. 반면, E2E 테스트(End-to-End Test) 는 사용자 관점에서 전체 시스템의 워크플로를 검증합니다. 예를 들어:
- 통합 테스트: 사용자 입력이 서비스 계층과 데이터베이스를 거쳐 올바른 데이터를 반환하는지 확인.
- E2E 테스트: 사용자가 로그인 버튼을 클릭하고 대시보드가 로드되는 전체 과정을 검증.
통합 테스트는 E2E 테스트보다 간단하고 유지보수가 용이하며, 필요에 따라 외부 의존성을 Mock으로 대체할 수 있습니다. 반면, E2E 테스트는 실제 사용자 관점에서 전체 시스템을 검증하므로 두 테스트를 균형 있게 활용하는 것이 중요합니다.
테스트 방법론
TDD (Test Driven Development)
TDD는 코드를 작성하기 이전에 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 구현함으로써 테스트된 동작 가능한 코드를 만들어내는 개발 방법입니다. TDD는 코드 품질을 높이고, 리팩터링을 용이하게 하며, 안정적인 소프트웨어 개발을 돕습니다.
테스트 주도 개발에 대한 여러가지 견해
TDD를 바라보는 관점은 다양합니다:
- 테스트 우선 개발 (Test-First Development) : 테스트를 먼저 작성하는 것으로 TDD를 정의합니다.
- 테스트 중심 개 발: 테스트를 많이 작성하는 것으로 간주합니다.
- 설계 방법론: 코드를 설계하고 동작을 점진적으로 발전시키는 도구로 TDD를 이해합니다.
DD는 단순한 "테스트 작성"을 넘어, 설계와 구현의 사이클을 유기적으로 결합하여 점진적으로 코드를 발전시키는 방법론으로 볼 수 있습니다.
일반적인 단위 테스트와 TDD 의 비교
TDD는 테스트 코드 작성과 기능 구현을 결합하여 개발 주기를 구성하는 접근 방식입니다. 이에 비해, 일반적인 단위 테스트는 이미 작성된 코드를 검증하기 위한 테스트 작성에 초점이 맞춰져 있습니다. 두 방법의 주요 차이점을 아래와 같이 정리할 수 있습니다.

| 특징 | 일반적인 단위 테스트 | TDD |
|---|---|---|
| 코드 작성 순서 | 코드 -> 테스트 | 테스트 ->코드 |
| 목적 | 코드 검증 | 설계 주도 및 구현 |
| 테스트 범위 | 개별 함수/메서드 중심 | 기능 요구사항 중심 |
| 리팩터링 | 필수가 아님(선택사항) | 필수 단계로 포함 |
| 개발 접근법 | 사후 검증 중심 | 설계와 구현을 동시에 진행 |

TDD 의 핵심 : Red-Green-Refactor 사이클
TDD는 다음과 같은 Red-Green-Refactor 주기를 반복하는 방식으로 진행됩니다:
- Red: 실패하는 테스트 작성
- 구현하려는 기능에 대한 (단위) 테스트를 작성합니다.
- 초기에는 테스트가 실패하도록 설계합니다. (즉, 아직 기능이 구현되지 않았음을 확인)
- Green: 테스트 통과를 위한 코드 작성
- 작성한 테스트를 통과하기 위해 필요한 최소한의 코드를 구현합니다.
- 테스트가 성공하는 것을 확인합니다.
- Refactor: 리팩터링
- 작성한 제품 코드와 테스트 코드를 개선합니다.
- 리팩터링 중에도 테스트가 통과하는지를 지속적으로 확인합니다.
이 주기를 반복하면서 코드를 점진적으로 발전시키고, 설계 품질을 개선할 수 있습니다.
입코딩의 끝판왕 TDD?
흔히 입코딩이라고 하는 대표주자를 TDD로 말하는 경우가 많은데 TDD는 만능이 아닙니다. 많은 사람들이 TDD를 “ 테스트를 위한 개발” 이라고 착 각하지만, TDD의 이상적인 모습은 “ 테스트로 설계를 주도하고, 안정적이고 깔끔한 코드를 만들어내는 것 ” 입니다.
TDD에 대한 몇가지 주의사항은 아래와 같습니다.
- 테스트를 위한 코드가 아니다
- TDD는 테스트를 위한 코드를 작성하는 것이 아니라, 테스트를 통해 요구사항을 충족하는 코드를 점진적으로 만들어가는 과정입니다.
- 테스트 과잉에 주의
- 모든 코드를 테스트하려다 보면, 비즈니스적으로 중요하지 않은 부분에 지나치게 많은 리소스를 소비할 수 있습니다. 핵심 로직과 주요 기능에 초점을 맞추어야 합니다.
- 예: 단순한 Getter/Setter 메서드 테스트, 외부 라이브러리나 기본 메서드(Node.js의 path.join 등)의 동작을 재검증, 의미 없는 데이터 시나리오를 지나치게 세분화한 테스트 등
- 지나치게 세분화 된 테스트를 지양
- 지나치게 작은 단위로 테스트를 작성하면, 코드 변경 시 과도한 수정이 필요해질 수 있습니다. 적절한 수준의 테스트 범위를 설정하는 것이 중요합니다.
- 리팩토링에 대한 두려움을 줄여야 한다
- TDD의 핵심은 리팩터링을 지속적으로 수행하는 것입니다. 테스트가 리팩터링을 지원하도록 설계하고, 리팩터링을 두려워하지 않아야 합니다.
- 팀의 합의와 표준화 필요
- 개인마다 테스트 작성 스타일이 다르거나, 팀 내 TDD 적용 기준이 명확하지 않으면 오히려 비효율이 발생할 수 있습니다. TDD를 도입하려면, 팀 내 테스트 작성 규칙과 목표를 공유하는 것이 중요합니다.
