테스트 코드를 깊이 있게 공부하면서, 단순히 기능이 잘 작동하는지 확인하는 것을 넘어 개발의 본질적인 측면을 다시 생각하게 되었습니다. 처음에는 코드를 하나 더 작성해야 한다는 점이 번거롭게 느껴졌지만, 단위 테스트, 통합 테스트, E2E 테스트의 명확한 역할과 그 차이를 이해하고 나니 모든 과정이 미래의 내가 겪을 잠재적인 버그와 유지보수의 고통을 덜어주기 때문에 중요한 과정이라는 생각이 들었습니다.
특히, 컴포넌트 단위 테스트를 통해 사용자의 관점에서 코드를 바라보는 연습을 하게 되었습니다. 이전에는 개발자로서의 시각에 갇혀 내부 로직만 신경 썼다면, 이제는 사용자가 버튼을 클릭하고 입력값을 넣는 등 실제 사용 경험을 고려하는 것의 중요성을 체감하게 되었습니다. 테스트코드를 작성하는 과정은 단순히 오류를 찾는 것을 넘어, 더욱 견고하고 사용자 친화적인 코드를 설계하는 데 필수적인 방법이라고 생각합니다. 앞으로는 이 지식을 바탕으로 더욱 책임감 있는 개발을 할 수 있을 것 같습니다.
테스트코드란?
테스트코드는 말 그대로 내가 작성한 코드가 의도한 대로 잘 동작하는지 검증하기 위해 작성하는 코드입니다. 일반적으로 애플리케이션의 특정 기능인 함수, 클래스, 모듈이 올바른 입력에 대해 예상된 출력을 내는지, 그리고 잘못된 입력이나 예외 상황에 대해 적절하게 처리하는지를 확인하는 목적으로 작성됩니다.
왜 테스트코드가 중요할까요?
테스트 코드는 코드가 정확하게 작동한다는 확신을 주고, 개발 과정 전반의 효율성과 안정성을 향상시킵니다.
1) 버그 예방 및 안정성 확보
테스트 코드는 방화벽과 같습니다. 사용자가 문제를 겪기 전에 잠재적인 버그를 미리 찾아내고 수정할 수 있게 해줍니다. 특히, 예상치 못한 엣지 케이스(edge case)나 예외 상황에 대한 테스트를 미리 작성해 두면, 실제 서비스에서 발생할 수 있는 치명적인 오류를 사전에 차단할 수 있습니다. 이는 곧 서비스의 신뢰성으로 이어져 사용자 경험을 크게 개선합니다.
2) 유지보수 및 리팩토링의 용이성
개발에서 가장 어려운 작업 중 하나는 기존 코드를 변경하는 것입니다. 기능 개선이나 리팩토링 시, 의도치 않게 다른 기능에 버그를 유발할 위험이 항상 존재할 때 테스트 코드가 좋은 안전망 역할을 합니다. 코드를 수정하고 테스트를 실행하기만 하면, 기존 기능이 여전히 제대로 작동하는지 자동으로 검증할 수 있습니다. 개발자가 코드 변경에 대한 부담을 줄이고 더 자신감 있게 작업할 수 있도록 돕습니다.
3) 개발 생산성 향상 및 자동화
테스트 코드는 반복적인 수동 테스트 과정을 자동화하여 개발 생산성을 높여줍니다. 예를 들어, UI 컴포넌트의 작은 수정 하나를 위해 매번 페이지를 새로고침하고 버튼을 일일이 클릭해 볼 필요가 없습니다. 테스트 코드를 실행하는 몇 초 만에 수많은 시나리오를 검증할 수 있어, 개발 주기를 단축시키고 개발자가 본질적인 문제 해결에 집중할 수 있게 해줍니다.
4) 문서화 및 협업 촉진
잘 작성된 테스트 코드는 그 자체가 코드의 동작 방식을 명확하게 설명하는 문서 역할을 합니다. 코드를 읽는 것만으로는 알기 힘든 함수의 입력값, 예상되는 출력값, 그리고 특정 기능의 시나리오를 테스트 코드를 통해 쉽게 파악할 수 있습니다. 새로운 팀원이 프로젝트에 빠르게 적응하도록 돕고, 개발자 간의 원활한 협업을 가능하게 합니다.
테스트코드의 종류
테스트코드는 검증하는 범위에 따라 크게 3가지로 나눌 수 있습니다.
- 단위 테스트 (Unit Test): 가장 작은 단위(함수, 메서드)를 독립적으로 테스트합니다.
ex) 두 숫자를 더하는 함수가 올바르게 작동하는지 확인하는 테스트 - 통합 테스트 (Integration Test): 여러 컴포넌트나 모듈이 서로 잘 상호작용하는지 테스트합니다.
ex) 사용자가 회원가입을 할 때 이메일 전송 기능이 올바르게 연동되는지 확인하는 테스트 - 종단 간 테스트 (End-to-End Test, E2E): 사용자의 관점에서 애플리케이션의 전체 흐름을 테스트합니다.
ex) 실제 사용자가 웹사이트에 접속하여 로그인을 하고, 상품을 구매하는 전 과정을 자동화하여 테스트
각각의 테스트 방식에 대한 코드 예시를 자세히 설명해 드리겠습니다.
1. 단위 테스트 (Unit Test)
애플리케이션의 가장 작은 단위(함수, 메서드, 컴포넌트)를 독립적으로 테스트합니다. 독립된 환경에서 오직 해당 단위의 로직만을 검증하는 것이 핵심입니다.
ex) 간단한 sum 함수 테스트
- 테스트 대상 코드 (sum.js)
export function sum(a, b) {
return a + b;
}
- 테스트 코드 (sum.test.js)
import { sum } from './sum';
// Jest의 describe 블록으로 테스트 그룹을 정의합니다.
describe('sum 함수', () => {
// it 블록으로 개별 테스트 케이스를 정의합니다.
test('두 숫자의 합을 올바르게 반환해야 한다', () => {
// expect: 테스트할 값을 지정합니다.
// toBe: 예상 값과 일치하는지 확인하는 matcher입니다.
expect(sum(1, 2)).toBe(3);
expect(sum(0, 0)).toBe(0);
expect(sum(-1, 1)).toBe(0);
});
});
이 테스트는 sum 함수가 다양한 입력값에 대해 항상 정확한 합을 반환하는지 검증합니다. 외부 요인 없이 순수하게 함수 자체의 논리만을 테스트하므로, 테스트 실행 속도가 매우 빠르고 디버깅이 용이합니다.
ex) React 컴포넌트 테스트
- 테스트 대상 컴포넌트 (Button.jsx)
import React from 'react';
export default function Button({ text, onClick }) {
return <button onClick={onClick}>{text}</button>;
}
- 테스트 코드 (Button.test.js)
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
test('버튼 컴포넌트가 올바르게 렌더링되고 클릭 이벤트를 처리해야 한다', () => {
// 1. 컴포넌트 렌더링
const handleClick = jest.fn(); // 클릭 이벤트를 mock 함수로 만듭니다.
render(<Button text="클릭하세요" onClick={handleClick} />);
// 2. 화면에서 특정 요소 찾기
const buttonElement = screen.getByText(/클릭하세요/i);
// 3. 사용자의 행동 시뮬레이션
fireEvent.click(buttonElement);
// 4. 예상 결과 검증
expect(buttonElement).toBeInTheDocument(); // 버튼이 문서에 있는지 확인
expect(handleClick).toHaveBeenCalledTimes(1); // 클릭 이벤트가 한 번 호출되었는지 확인
});
이 테스트는 사용자의 관점에서 Button 컴포넌트가 보이는 대로 작동하는지 검증합니다. render 함수는 컴포넌트를 DOM에 렌더링하고, fireEvent는 사용자의 클릭, 타이핑 등 이벤트를 모방합니다. jest.fn()으로 만든 모킹 함수는 실제 onClick이 아닌 가짜 함수를 연결하여, 컴포넌트의 내부 로직만을 독립적으로 테스트할 수 있게 합니다.
2. 통합 테스트 (Integration Test)
여러 컴포넌트나 모듈이 결합하여 상호작용하는 흐름을 테스트합니다. 부품들이 잘 조립되어 원하는 기능을 수행하는지 확인합니다.
ex) 검색 필터링 기능 테스트 - SearchInput 컴포넌트와 SearchResult 컴포넌트의 통합 테스트
- 테스트 대상 컴포넌트 (SearchApp.jsx)
import React, { useState } from 'react';
import SearchInput from './SearchInput';
import SearchResult from './SearchResult';
export default function SearchApp() {
const [query, setQuery] = useState('');
const results = ['apple', 'banana', 'cherry'].filter(item =>
item.includes(query)
);
return (
<div>
<SearchInput value={query} onChange={e => setQuery(e.target.value)} />
<SearchResult results={results} />
</div>
);
}
- 테스트 코드 (SearchApp.test.js)
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import SearchApp from './SearchApp';
test('검색어를 입력하면 결과가 올바르게 필터링되어야 한다', () => {
render(<SearchApp />);
// 1. 검색 input을 찾고 값 입력
const searchInput = screen.getByRole('textbox');
fireEvent.change(searchInput, { target: { value: 'a' } });
// 2. 결과가 올바르게 렌더링되었는지 확인
expect(screen.getByText('apple')).toBeInTheDocument();
expect(screen.getByText('banana')).toBeInTheDocument();
// 'cherry'는 'a'를 포함하지 않으므로 화면에 없어야 함
expect(screen.queryByText('cherry')).not.toBeInTheDocument();
});
이 테스트는 SearchInput 컴포넌트의 onChange 이벤트가 SearchApp 컴포넌트의 query 상태를 변경하고, 그 상태 변경이 SearchResult 컴포넌트의 렌더링에 올바르게 반영되는 전체적인 데이터 흐름을 검증합니다. 각 컴포넌트의 내부 구현보다는 통합된 기능에 초점을 맞춥니다.
3. E2E 테스트 (End-to-End Test)
실제 사용자가 애플리케이션을 사용하는 것과 동일하게, 처음부터 끝까지 전체 시나리오를 테스트합니다. 가장 큰 범위의 테스트로, 프론트엔드부터 백엔드, 데이터베이스까지 모든 시스템의 연동을 검증합니다. E2E 테스트는 별도의 도구(Cypress, Playwright 등)를 사용하며, 테스트 코드가 실제 브라우저를 제어합니다.
ex) 사용자 로그인 흐름 테스트 (Cypress 사용)
- 테스트 코드 (login.cy.js)
// describe와 it은 Jest와 비슷하지만, Cypress 환경에서 실행됩니다.
describe('로그인 기능 테스트', () => {
it('유효한 자격 증명으로 로그인에 성공해야 한다', () => {
// 1. 웹사이트 접속
cy.visit('<http://localhost:3000/login>');
// 2. 입력 필드에 값 입력
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="password"]').type('password123');
// 3. 로그인 버튼 클릭
cy.get('button[type="submit"]').click();
// 4. 예상 결과 검증 (예: 대시보드 페이지로 리디렉션되었는지 확인)
cy.url().should('include', '/dashboard');
cy.contains('환영합니다, testuser!').should('be.visible');
});
it('잘못된 비밀번호로 로그인 실패 시 에러 메시지가 표시되어야 한다', () => {
// 1. 웹사이트 접속
cy.visit('<http://localhost:3000/login>');
// 2. 잘못된 값 입력
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="password"]').type('wrongpassword');
// 3. 로그인 버튼 클릭
cy.get('button[type="submit"]').click();
// 4. 에러 메시지 검증
cy.contains('잘못된 비밀번호입니다.').should('be.visible');
});
});
E2E 테스트는 코드의 내부 로직보다는 시스템 전체의 동작에 집중합니다. 이 테스트는 프론트엔드와 백엔드 서버가 모두 실행 중인 상태에서 진행되며, 데이터베이스와 같은 외부 시스템과의 연동까지 포괄적으로 검증합니다. 이처럼 실제 사용자 경험을 가장 잘 시뮬레이션할 수 있지만, 테스트 환경 구축이 복잡하고 실행 시간이 오래 걸리는 단점이 있습니다.
references
- https://techblog.woowahan.com/17404/
- https://techblog.woowahan.com/17721/
- https://ko.legacy.reactjs.org/docs/testing.html
'👩🏻💻 Develop > Test' 카테고리의 다른 글
| [Next.js] Jest와 React Testing Library로 단위 테스트코드 작성하기 (0) | 2025.09.23 |
|---|