테스트는 귀찮은 일이 아니라, 확인을 자동화하는 일입니다
처음 테스트 코드를 배우려고 하면 이런 생각이 들기 쉽습니다.
"이미 브라우저에서 눌러 봤는데 왜 또 테스트를 쓰지?"
"함수가 잘 동작하는지 console.log로 확인하면 되는 것 아닌가?"
"테스트 코드를 작성하는 시간에 기능을 하나 더 만드는 게 낫지 않을까?"
이 질문들은 아주 자연스럽습니다. 테스트를 처음 배우는 개발자라면 누구나 한 번쯤 떠올립니다. 실제로 작은 프로그램을 만들 때는 직접 실행해 보고, 버튼을 눌러 보고, 콘솔에 값을 찍어 보는 것만으로도 충분해 보입니다.
하지만 프로그램은 생각보다 빨리 커집니다. 처음에는 계산기 함수 하나였던 코드가 나중에는 로그인, 결제, 장바구니, 관리자 페이지, 외부 API 연동까지 이어집니다. 이때 모든 기능을 사람이 매번 직접 확인하는 일은 점점 불가능에 가까워집니다.
테스트 코드는 특별한 마법이 아닙니다. 사람이 손으로 하던 확인을 코드로 적어 두는 일입니다. 한 번 적어 둔 확인 절차는 필요할 때마다 자동으로 반복할 수 있습니다. 그래서 테스트를 배우는 핵심은 문법을 외우는 것이 아니라, "내가 지금 무엇을 확인하고 싶은가?"를 코드로 표현하는 법을 익히는 것입니다.
Jest는 이 일을 도와주는 대표적인 JavaScript 테스트 도구입니다. 이 책에서는 Jest를 사용해 테스트의 기본 개념부터 실무에서 자주 마주치는 상황까지 차근차근 배워 나갑니다.

수동 확인과 자동화 테스트의 차이
아주 간단한 계산기 함수를 떠올려 보겠습니다.
function add(a, b) {
return a + b;
}이 함수가 제대로 동작하는지 확인하려면 어떻게 할 수 있을까요? 가장 쉬운 방법은 직접 실행해 보는 것입니다.
console.log(add(1, 2)); // 3이 나와야 함
console.log(add(10, 5)); // 15가 나와야 함처음에는 이 방식도 괜찮습니다. 눈으로 결과를 보고 "음, 잘 되네"라고 판단할 수 있습니다. 이것이 수동 확인입니다. 사람이 직접 실행하고, 직접 결과를 보고, 직접 맞는지 판단합니다.
그런데 나중에 add 함수가 다른 코드와 연결되고, 내부 구현이 바뀌었다고 생각해 봅시다. 우리는 다시 console.log를 실행하고 결과를 확인해야 합니다. 함수가 하나라면 괜찮지만, 함수가 100개라면 어떨까요? 각 함수마다 다양한 입력값을 확인해야 한다면 어떨까요?
자동화 테스트는 이 확인 과정을 코드로 고정해 둡니다.
test('두 숫자를 더한다', () => {
expect(add(1, 2)).toBe(3);
});지금은 문법을 완전히 이해하지 않아도 됩니다. 중요한 것은 문장의 의미입니다.
"add(1, 2)를 실행했을 때 결과가 3이어야 한다."
이렇게 적어 두면 Jest가 대신 실행하고, 대신 비교하고, 대신 알려 줍니다. 결과가 맞으면 통과했다고 알려 주고, 틀리면 어디에서 기대와 달랐는지 보여 줍니다.
수동 확인은 그때그때 사람이 기억해서 반복해야 합니다. 자동화 테스트는 확인할 내용을 코드에 저장해 두고, 기계가 반복하게 만듭니다.
UI 변경은 왜 더 위험할까요?
계산기 함수처럼 입력과 출력이 분명한 코드는 비교적 확인하기 쉽습니다. 하지만 실제 서비스에서는 UI 변경이 자주 일어납니다. 버튼 문구를 바꾸고, CSS 클래스를 수정하고, 컴포넌트를 나누고, 화면 구조를 정리합니다.
예를 들어 쇼핑몰에 "장바구니에 담기" 버튼이 있다고 해 봅시다. 사용자가 버튼을 누르면 상품이 장바구니에 추가되어야 합니다. 개발자는 브라우저를 열고 상품 페이지로 이동한 뒤, 버튼을 누르고, 장바구니 숫자가 증가하는지 확인할 수 있습니다.
문제는 이 확인이 한 번으로 끝나지 않는다는 데 있습니다. 버튼 색상을 바꿨을 때도 확인해야 합니다. 상품 카드 컴포넌트를 리팩터링했을 때도 확인해야 합니다. 장바구니 상태 관리 방식을 바꿨을 때도 확인해야 합니다. 심지어 전혀 관련 없어 보이는 헤더 컴포넌트를 수정했는데 장바구니 표시가 깨질 수도 있습니다.
수동 확인은 이런 상황에서 점점 느슨해집니다. 처음에는 꼼꼼하게 눌러 보지만, 시간이 지나면 "이건 괜찮겠지" 하고 넘어가기 쉽습니다. 사람은 피곤해지고, 일정은 바빠지고, 확인 목록은 길어집니다.
자동화 테스트는 여기서 힘을 발휘합니다. 중요한 동작을 테스트 코드로 적어 두면, UI를 고치더라도 핵심 동작이 여전히 유지되는지 빠르게 확인할 수 있습니다. 테스트는 개발자를 의심하기 위해 존재하는 것이 아닙니다. 개발자가 안심하고 코드를 바꾸도록 도와주기 위해 존재합니다.
테스트는 무엇을 어디까지 확인할까요?
테스트라고 해서 모두 같은 범위를 확인하는 것은 아닙니다. 어떤 테스트는 작은 함수 하나만 확인하고, 어떤 테스트는 여러 모듈이 함께 동작하는지 확인합니다. 또 어떤 테스트는 실제 사용자가 브라우저에서 서비스를 이용하는 흐름 전체를 확인합니다.
처음에는 세 가지 정도만 구분하면 충분합니다.
단위 테스트
통합 테스트
E2E 테스트
단위 테스트는 가장 작은 단위의 코드를 확인합니다. 보통 함수 하나, 클래스의 메서드 하나, 또는 작은 컴포넌트 하나를 대상으로 합니다. 예를 들어 add(1, 2)가 3을 반환하는지 확인하는 테스트는 단위 테스트에 가깝습니다. 범위가 작기 때문에 빠르게 실행되고, 실패했을 때 원인을 찾기도 쉽습니다.
통합 테스트는 여러 조각이 함께 잘 동작하는지 확인합니다. 예를 들어 회원가입 폼에서 사용자가 이메일과 비밀번호를 입력하고 제출했을 때, 입력값 검증 함수와 상태 관리 코드와 API 호출 함수가 의도대로 연결되는지 볼 수 있습니다. 단위 테스트가 부품 하나를 보는 것이라면, 통합 테스트는 부품 몇 개를 조립했을 때 제대로 맞물리는지 보는 것입니다.
E2E 테스트는 End-to-End 테스트의 줄임말입니다. 말 그대로 처음부터 끝까지 사용자의 실제 흐름에 가깝게 확인합니다. 예를 들어 사용자가 브라우저에서 로그인 페이지에 들어가고, 아이디와 비밀번호를 입력하고, 로그인 버튼을 누르고, 메인 화면으로 이동하는 전체 과정을 테스트할 수 있습니다.
세 테스트는 서로 경쟁 관계가 아닙니다. 역할이 다릅니다. 단위 테스트는 작은 실수를 빠르게 잡는 데 강하고, 통합 테스트는 연결 문제를 찾는 데 유용하며, E2E 테스트는 실제 사용자 관점의 큰 흐름을 확인하는 데 좋습니다.
테스트 피라미드: 작고 빠른 테스트가 먼저입니다
테스트를 처음 배울 때 흔히 하는 오해가 있습니다. "실제 사용자처럼 브라우저에서 전부 확인하는 테스트가 가장 좋은 테스트 아닌가?"라는 생각입니다. 물론 사용자 흐름을 확인하는 테스트는 중요합니다. 하지만 모든 테스트를 E2E 테스트로만 만들면 곧 불편해집니다.
E2E 테스트는 많은 것을 한 번에 확인하는 대신 느리고 복잡합니다. 브라우저를 띄워야 하고, 서버 상태나 네트워크 상태의 영향을 받을 수 있습니다. 실패했을 때도 원인을 찾기가 어렵습니다. 로그인 테스트가 실패했을 때, 입력 폼이 문제인지, 인증 API가 문제인지, 데이터베이스 상태가 문제인지 한눈에 알기 어렵습니다.
그래서 테스트 피라미드라는 관점이 자주 사용됩니다. 아래에는 빠르고 작은 단위 테스트를 많이 두고, 중간에는 통합 테스트를 적당히 두고, 위에는 E2E 테스트를 꼭 필요한 만큼 두는 방식입니다.
피라미드의 아래쪽이 넓은 이유는 단순합니다. 작은 테스트는 실행이 빠르고 작성 부담이 낮습니다. 실패했을 때 원인도 비교적 분명합니다. 이런 테스트가 많으면 개발자는 코드를 고칠 때마다 즉시 피드백을 받을 수 있습니다.
예를 들어 가격 계산 함수가 잘못되었다면 단위 테스트가 바로 알려 줄 수 있습니다. 장바구니와 쿠폰 계산이 함께 연결되는 부분에서 문제가 생겼다면 통합 테스트가 잡아 줄 수 있습니다. 결제까지 이어지는 전체 흐름이 깨졌다면 E2E 테스트가 알려 줄 수 있습니다.
좋은 테스트 전략은 모든 것을 한 종류의 테스트로 해결하려 하지 않습니다. 작은 문제는 작은 테스트로 빠르게 확인하고, 큰 흐름은 큰 테스트로 확인합니다. 이 균형을 이해하면 Jest를 왜 배우는지도 훨씬 선명해집니다. Jest는 특히 단위 테스트와 통합 테스트를 작성하고 실행하는 데 많이 사용됩니다.
테스트가 주는 가장 현실적인 이점
테스트의 장점은 "코드 품질이 좋아진다"처럼 추상적으로 말할 수도 있습니다. 하지만 초보자에게는 더 현실적인 이유가 필요합니다. 테스트를 쓰면 실제로 무엇이 좋아질까요?
첫째, 버그를 더 일찍 발견할 수 있습니다. 버그는 늦게 발견될수록 고치기 어렵습니다. 방금 작성한 함수에서 생긴 버그는 코드 몇 줄만 살펴보면 됩니다. 하지만 그 버그가 배포 후 사용자 신고로 발견되면 상황이 복잡해집니다. 어떤 입력에서 문제가 생겼는지, 어떤 브라우저에서 발생했는지, 어느 배포 이후부터 깨졌는지 추적해야 합니다. 테스트는 버그가 멀리 도망가기 전에 붙잡아 주는 그물입니다.
둘째, 회귀를 막을 수 있습니다. 회귀란 예전에는 잘 되던 기능이 나중에 다시 깨지는 것을 말합니다. 개발을 하다 보면 새 기능을 추가하면서 기존 기능을 망가뜨리는 일이 생깁니다. 일부러 그런 것이 아닙니다. 코드끼리 연결되어 있기 때문입니다. 테스트가 있으면 예전에 고친 버그나 중요한 기능이 다시 깨졌을 때 빠르게 알 수 있습니다.
셋째, 리팩터링할 자신감이 생깁니다. 리팩터링은 겉으로 보이는 기능은 그대로 두고, 내부 코드를 더 읽기 좋고 유지보수하기 쉽게 바꾸는 작업입니다. 테스트가 없으면 리팩터링은 꽤 무서운 일이 됩니다. "이 부분을 고쳐도 정말 괜찮을까?"라는 불안이 따라옵니다. 반대로 테스트가 있으면 코드를 정리한 뒤 테스트를 실행해 볼 수 있습니다. 테스트가 통과하면 적어도 우리가 약속해 둔 동작은 유지되고 있다는 뜻입니다.
테스트는 개발 속도를 늦추는 장치가 아닙니다. 처음에는 조금 느려지는 것처럼 느껴질 수 있습니다. 하지만 프로젝트가 커질수록 테스트는 속도를 지켜 주는 안전장치가 됩니다. 매번 모든 것을 손으로 확인하지 않아도 되기 때문입니다.
Jest는 무엇을 해결해 줄까요?
이제 Jest가 어떤 도구인지 큰 그림을 잡아 봅시다.
Jest는 JavaScript와 TypeScript 프로젝트에서 널리 사용되는 테스트 프레임워크입니다. 프레임워크라는 말이 조금 어렵게 느껴질 수 있습니다. 여기서는 "테스트를 작성하고 실행하는 데 필요한 여러 기능을 한 번에 제공하는 도구 모음" 정도로 이해하면 됩니다.
테스트를 제대로 실행하려면 생각보다 많은 것이 필요합니다. 테스트 파일을 찾아 실행해야 하고, 실제 결과와 기대 결과를 비교해야 합니다. 외부 API나 데이터베이스처럼 테스트하기 까다로운 대상은 가짜로 대체할 수도 있어야 합니다. 또 코드의 어느 부분이 테스트되었고 어느 부분은 아직 확인되지 않았는지도 알고 싶어집니다.
Jest는 이런 기능들을 한곳에 모아 제공합니다.
테스트 러너: 테스트 파일을 찾아 실행하고 결과를 보여 줍니다.
Assertion: 실제 결과가 기대한 값과 같은지 검증합니다.
Mocking: 함수, 모듈, 외부 의존성을 가짜로 대체해 테스트를 쉽게 만듭니다.
Coverage: 테스트가 코드의 어느 부분을 실행했는지 확인할 수 있게 해 줍니다.
테스트 러너는 말 그대로 테스트를 실행하는 역할을 합니다. 우리가 npm test 같은 명령어를 입력하면 Jest가 테스트 파일을 찾고, 각 테스트를 실행한 뒤, 어떤 테스트가 통과했고 어떤 테스트가 실패했는지 알려 줍니다.
Assertion은 "검증"입니다. 예를 들어 expect(result).toBe(3)은 result가 3이어야 한다고 선언하는 코드입니다. 테스트의 핵심은 이 검증에 있습니다. 단순히 함수를 실행하는 것이 아니라, 그 결과가 우리가 기대한 것과 일치하는지 확인해야 하기 때문입니다.
Mocking은 처음에는 낯설 수 있지만 매우 유용합니다. 테스트하고 싶은 코드가 외부 API를 호출한다고 해 봅시다. 매번 진짜 서버에 요청을 보내면 테스트가 느려지고 불안정해질 수 있습니다. 서버가 잠시 느리거나 네트워크가 끊기면 내 코드가 멀쩡해도 테스트가 실패할 수 있습니다. 이럴 때 Jest의 Mock 기능을 사용하면 외부 API를 가짜 응답으로 대체할 수 있습니다. 그러면 우리는 "외부 세계"가 아니라 "내 코드의 판단"에 집중할 수 있습니다.
Coverage는 테스트 범위를 보여 줍니다. 예를 들어 전체 코드 중 몇 퍼센트가 테스트 실행 중에 지나갔는지 알려 줍니다. 커버리지가 높다고 무조건 좋은 테스트인 것은 아니지만, 테스트가 전혀 닿지 않는 위험한 영역을 찾는 데 도움이 됩니다.
테스트 코드는 작은 약속입니다
테스트를 어렵게 만드는 이유 중 하나는 처음부터 완벽하게 쓰려고 하기 때문입니다. 모든 경우를 다 테스트해야 할 것 같고, 멋진 구조로 작성해야 할 것 같고, 실무에서 쓰는 복잡한 패턴을 바로 알아야 할 것 같습니다.
하지만 시작은 단순해야 합니다. 테스트 코드는 작은 약속입니다.
"이 함수는 두 숫자를 더해야 한다."
"로그인하지 않은 사용자는 주문 페이지에 들어갈 수 없어야 한다."
"장바구니에 상품을 담으면 개수가 1 증가해야 한다."
이런 약속을 하나씩 코드로 적어 두는 것이 테스트의 시작입니다. 나중에 코드가 바뀌어도 이 약속들이 지켜지는지 확인할 수 있습니다.
테스트가 많아지면 프로젝트는 조금씩 다른 성격을 갖게 됩니다. 코드를 고칠 때마다 막연한 불안 대신 확인할 수 있는 기준이 생깁니다. 새 기능을 추가할 때 기존 기능이 깨지지 않았는지 빠르게 확인할 수 있습니다. 버그를 고친 뒤에는 같은 버그가 다시 나타나지 않도록 테스트로 잠가 둘 수 있습니다.
이 책에서 배울 흐름
이 책은 Jest를 처음 접하는 개발자를 기준으로 진행합니다. 처음부터 복잡한 설정이나 어려운 테스트 전략을 다루지 않습니다. 먼저 테스트가 왜 필요한지 이해하고, Jest 개발 환경을 준비한 뒤, 가장 작은 테스트부터 작성합니다.
그다음 테스트 코드를 읽기 좋게 구성하는 방법, 다양한 값을 검증하는 Matcher, Jest 실행 옵션, 비동기 코드 테스트, Setup과 Teardown, Mock 함수와 모듈 Mocking을 차례대로 살펴봅니다. 이후에는 시간 관련 코드, Snapshot 테스트, 코드 커버리지, React 컴포넌트 테스트, TypeScript와 Babel 설정, CI 환경에서의 자동화까지 확장해 나갑니다.
중요한 것은 모든 내용을 한 번에 완벽히 이해하는 것이 아닙니다. 테스트는 운동과 비슷합니다. 처음에는 자세가 어색하지만, 작은 동작을 반복하다 보면 어느 순간 자연스러워집니다. expect 하나로 시작한 테스트가 나중에는 프로젝트를 안전하게 지탱하는 중요한 습관이 됩니다.
다음 장에서는 Jest를 직접 사용할 수 있도록 개발 환경을 준비합니다. 아직 테스트 문법을 많이 몰라도 괜찮습니다. 지금 기억해야 할 것은 하나입니다.
테스트 코드는 개발자를 귀찮게 하는 숙제가 아니라, 미래의 나를 도와주는 자동 확인 장치입니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
