
테스트 구조 및 워크플로 상세 가이드
3.1 기본 구성 - AAA 패턴 심화 이해
AAA 패턴은 1950년대부터 시작된 소프트웨어 테스팅의 핵심 구조로, 2001년 Bill Wake에 의해 체계화되고 2002년 Kent Beck의 “Test Driven Development: By Example”에서 널리 알려진 테스트 작성 방법론입니다12. 이 패턴은 테스트의 가독성과 유지보수성을 크게 향상시키며, BDD(Behavior-Driven Development)의 Given-When-Then 구조와 밀접한 관련이 있습니다13.
[asset:1]
**Arrange (준비)테스트 환경을 설정하는 가장 중요한 단계로, 테스트 대상 유닛과 모든 의존성을 격리하여 통제된 환경을 만드는 과정입니다14. 이 단계에서는 다음과 같은 작업을 수행합니다:
- 객체 인스턴스 생성: 테스트할 클래스나 컴포넌트의 인스턴스 생성
- Mock/Stub 객체 설정: 외부 의존성을 가짜 객체로 대체
- 테스트 데이터 준비: 입력값, 예상 결과값 등 테스트에 필요한 데이터 설정
- 환경 설정: 데이터베이스 상태, 파일 시스템, 네트워크 설정 등
// Next.js 컴포넌트 테스트에서의 Arrange 예시
describe('LoginForm 컴포넌트', () => {
test('로그인 성공 시 대시보드로 이동한다', async () => {
// Arrange - 복잡한 준비 단계
const mockRouter = { push: jest.fn() };
const mockAuthService = {
login: jest.fn().mockResolvedValue({
token: 'test-token',
user: { id: 1, name: 'Test User' }
})
};
// Mock Next.js 라우터
jest.spyOn(require('next/router'), 'useRouter').mockReturnValue(mockRouter);
// 컴포넌트 렌더링
render(<LoginForm authService={mockAuthService} />);
// 필요한 DOM 요소 가져오기
const emailInput = screen.getByLabelText('이메일');
const passwordInput = screen.getByLabelText('비밀번호');
const submitButton = screen.getByRole('button', { name: '로그인' });
중요한 특징: Arrange 단계는 종종 가장 긴 코드 분량을 차지하며, 반복되는 설정이 많다면 beforeEach
나 별도의 헬퍼 함수로 추출하는 것이 좋습니다12.
Act (실행) 단계
테스트하고자 하는 핵심 동작을 수행하는 단계로, 보통 한 줄의 코드로 구성됩니다12. 두 줄 이상의 코드가 필요하다면 API 설계를 재검토해야 할 신호일 수 있습니다2.
// Act - 실제 사용자 동작 시뮬레이션
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
Assert (검증) 단계
Act 단계의 결과가 예상한 대로 동작했는지 확인하는 단계입니다12. 단일 동작이더라도 여러 결과가 나올 수 있기 때문에 여러 개의 검증문을 사용할 수 있습니다:
// Assert - 다양한 결과 검증
await waitFor(() => {
expect(mockAuthService.login).toHaveBeenCalledWith('test@example.com', 'password123');
expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
expect(screen.queryByText('로그인 중...')).not.toBeInTheDocument();
});
});
});
3.2 테스트 더블(Test Double) 완전 이해
테스트 더블은 영화의 스턴트 더블에서 유래된 용어로, 실제 객체를 대신하는 가짜 객체를 의미합니다56. Gerard Meszaros의 분류에 따라 5가지 유형으로 나뉩니다6.
[asset:3]
Mock (목) - 행위 검증 객체
함수나 메서드의 호출 여부, 호출 횟수, 호출 인자를 검증하는 데 사용됩니다57. Mock은 상호작용 검증에 초점을 맞춘 테스트 더블입니다89.
// Next.js에서 API 호출 Mock 예시
test('게시글 삭제 시 API가 올바르게 호출된다', async () => {
// Arrange
const mockApiClient = {
delete: jest.fn().mockResolvedValue({ success: true })
};
render(<PostList apiClient={mockApiClient} />);
// Act
fireEvent.click(screen.getByTestId('delete-post-1'));
// Assert - Mock을 통한 행위 검증
expect(mockApiClient.delete).toHaveBeenCalledWith('/api/posts/1');
expect(mockApiClient.delete).toHaveBeenCalledTimes(1);
});
Mock의 특징:
Stub (스텁) - 상태 검증 객체
미리 정의된 응답을 제공하여 테스트 대상이 올바른 로직을 수행하는지 확인합니다57. Stub은 상태 검증에 초점을 맞춘 테스트 더블입니다89.
// Next.js API 응답 Stub 예시
test('사용자 목록을 올바르게 표시한다', async () => {
// Arrange - Stub으로 고정된 응답 제공
const userServiceStub = {
getUsers: jest.fn().mockResolvedValue([
{ id: 1, name: '홍길동', email: 'hong@test.com' },
{ id: 2, name: '김철수', email: 'kim@test.com' }
])
};
render(<UserList userService={userServiceStub} />);
// Assert - 상태 검증
await waitFor(() => {
expect(screen.getByText('홍길동')).toBeInTheDocument();
expect(screen.getByText('김철수')).toBeInTheDocument();
});
});
Fake (페이크) - 간소화된 실제 구현체
실제 동작을 모방하지만 더 단순하게 구현된 객체입니다59. 프로덕션에는 적합하지 않지만 테스트에서는 실제와 유사한 동작을 제공합니다6.
// In-Memory Database Fake 예시
class FakeUserRepository {
constructor() {
this.users = new Map();
this.nextId = 1;
}
async create(userData) {
const user = { id: this.nextId++, ...userData };
this.users.set(user.id, user);
return user;
}
async findById(id) {
return this.users.get(id) || null;
}
async update(id, updates) {
const user = this.users.get(id);
if (user) {
Object.assign(user, updates);
return user;
}
return null;
}
}
Fake 객체의 장점:
- 복잡한 외부 의존성 완전 대체 가능
- 실제와 유사한 동작 제공으로 더 현실적인 테스트
- 빠른 실행 속도와 안정적인 테스트 환경
3.3 파라미터라이즈드 테스트 심화
파라미터라이즈드 테스트는 동일한 테스트 로직을 여러 입력값으로 반복 실행하여 코드 중복을 줄이고 테스트 커버리지를 높이는 강력한 기법입니다1011. 이는 Data-Driven Testing이라고도 불리며, 1997년 JUnit에서 처음 도입된 이후 모든 주요 테스팅 프레임워크에서 지원하고 있습니다11.
[asset:5]
Jest에서의 파라미터라이즈드 테스트 구현
Jest는 test.each()
와 describe.each()
함수를 통해 파라미터라이즈드 테스트를 지원합니다1213.
1. 기본 배열 방식
// 계산기 함수 테스트
describe('사칙연산 테스트', () => {
test.each([
[2, 3, 5], // 2 + 3 = 5
[10, -5, 5], // 10 + (-5) = 5
[0, 0, 0], // 0 + 0 = 0
[-1, 1, 0] // -1 + 1 = 0
])('add(%i, %i)는 %i를 반환한다', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
2. 테이블 형식 (템플릿 리터럴)
// 입력 유효성 검증 테스트
describe('이메일 유효성 검사', () => {
test.each`
email | expected | description
${'user@example.com'} | ${true} | ${'정상적인 이메일'}
${'invalid-email'} | ${false} | ${'@가 없는 이메일'}
${'test@'} | ${false} | ${'도메인이 없는 이메일'}
${'@domain.com'} | ${false} | ${'사용자명이 없는 이메일'}
${''} | ${false} | ${'빈 문자열'}
`('$description: validateEmail("$email") → $expected',
({ email, expected, description }) => {
expect(validateEmail(email)).toBe(expected);
}
);
});
3. 외부 데이터 소스 활용
// testData.json 파일에서 데이터 로드
const testData = require('./testData.json');
describe('사용자 권한 검증', () => {
test.each(testData.userPermissions)(
'$role 역할의 사용자는 $resource에 $access 권한을 가진다',
({ role, resource, access, expected }) => {
const user = createUser(role);
expect(user.canAccess(resource, access)).toBe(expected);
}
);
});
Next.js 프로젝트에서의 실제 활용 사례
[asset:4]
API 라우트 테스트
// 다양한 HTTP 메서드 테스트
describe('/api/users API 엔드포인트', () => {
test.each([
['GET', {}, 200, '사용자 목록 조회'],
['POST', { name: 'Test User', email: 'test@example.com' }, 201, '사용자 생성'],
['PUT', { id: 1, name: 'Updated User' }, 200, '사용자 정보 수정'],
['DELETE', { id: 1 }, 204, '사용자 삭제']
])('%s 요청: %s', async (method, body, expectedStatus, description) => {
const { req, res } = createMocks({
method,
body: JSON.stringify(body)
});
await handler(req, res);
expect(res._getStatusCode()).toBe(expectedStatus);
});
});
컴포넌트 props 검증
// 버튼 컴포넌트의 다양한 상태 테스트
describe('Button 컴포넌트', () => {
test.each([
['primary', 'bg-blue-500', 'text-white'],
['secondary', 'bg-gray-500', 'text-white'],
['danger', 'bg-red-500', 'text-white'],
['success', 'bg-green-500', 'text-white']
])('%s 타입 버튼은 올바른 스타일을 적용한다', (variant, bgClass, textClass) => {
render(<Button variant={variant}>테스트</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass(bgClass, textClass);
});
});
파라미터라이즈드 테스트의 핵심 가치
- 코드 중복 제거: 동일한 테스트 로직을 반복 작성할 필요 없음1014
- 테스트 커버리지 향상: 다양한 입력값으로 광범위한 시나리오 테스트1514
- 유지보수 효율성: 테스트 로직 변경 시 한 곳만 수정하면 됨1416
- 가독성 향상: 테스트 데이터와 로직이 명확히 분리됨1015
- 경계값 테스트: 다양한 edge case를 체계적으로 검증 가능1718
사용 시기:
- 동일한 로직을 여러 입력값으로 테스트해야 할 때1015
- 폼 유효성 검증, API 응답 처리, 계산 로직 등을 테스트할 때1718
- Cross-browser, 다양한 설정값 테스트가 필요할 때14
- 대량의 테스트 데이터를 효율적으로 관리해야 할 때1920
파라미터라이즈드 테스트는 테스트의 품질과 효율성을 동시에 향상시키는 강력한 도구로, 특히 Next.js와 같은 복잡한 웹 애플리케이션에서 다양한 시나리오를 체계적으로 검증하는 데 필수적인 기법입니다1112.
Footnotes
-
https://semaphore.io/blog/aaa-pattern-test-automation ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
https://docs.telerik.com/devtools/justmock/basic-usage/arrange-act-assert ↩ ↩2 ↩3 ↩4 ↩5
-
https://automationpanda.com/2020/07/07/arrange-act-assert-a-pattern-for-writing-good-tests/ ↩
-
https://circleci.com/blog/how-to-test-software-part-i-mocking-stubbing-and-contract-testing/ ↩ ↩2
-
https://flambeeyoga.tistory.com/entry/Test-Double과-Stub-Mock-그리고-Fake ↩ ↩2 ↩3 ↩4
-
https://www.getxray.app/blog/maximize-your-coverage-with-test-paramtererization-and-data-driven-testing ↩ ↩2 ↩3 ↩4
-
https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html ↩ ↩2
-
https://www.browserstack.com/guide/jest-parameterized-test ↩
-
https://www.getxray.app/blog/test-parameterization-techniques ↩ ↩2 ↩3 ↩4
-
https://www.linkedin.com/pulse/data-driven-testing-parameterization-tools-elise-lowry ↩ ↩2 ↩3
-
https://www.aiotests.com/blog/what-is-parameterization-in-testing ↩
-
https://docs.qase.io/general/get-started-with-the-qase-platform/test-cases/test-case-parameters ↩ ↩2
-
https://www.zoho.com/qengine/know/data-driven-testing.html ↩ ↩2
-
https://jazzteam.org/technical-articles/data-driven-testing/ ↩
-
https://smartbear.com/blog/your-guide-to-data-driven-testing/ ↩