테스트 구조 및 워크플로 상세 가이드

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의 특징:

  • 행위 기반 테스트에 적합9
  • 메서드 호출이 예상대로 이루어졌는지 확인
  • 구현부와 결합도가 높아 리팩토링 시 주의 필요6

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);
  });
});

파라미터라이즈드 테스트의 핵심 가치

  1. 코드 중복 제거: 동일한 테스트 로직을 반복 작성할 필요 없음1014
  2. 테스트 커버리지 향상: 다양한 입력값으로 광범위한 시나리오 테스트1514
  3. 유지보수 효율성: 테스트 로직 변경 시 한 곳만 수정하면 됨1416
  4. 가독성 향상: 테스트 데이터와 로직이 명확히 분리됨1015
  5. 경계값 테스트: 다양한 edge case를 체계적으로 검증 가능1718

사용 시기:

  • 동일한 로직을 여러 입력값으로 테스트해야 할 때1015
  • 폼 유효성 검증, API 응답 처리, 계산 로직 등을 테스트할 때1718
  • Cross-browser, 다양한 설정값 테스트가 필요할 때14
  • 대량의 테스트 데이터를 효율적으로 관리해야 할 때1920

파라미터라이즈드 테스트는 테스트의 품질과 효율성을 동시에 향상시키는 강력한 도구로, 특히 Next.js와 같은 복잡한 웹 애플리케이션에서 다양한 시나리오를 체계적으로 검증하는 데 필수적인 기법입니다1112.

Footnotes

  1. https://semaphore.io/blog/aaa-pattern-test-automation 2 3 4 5 6

  2. https://docs.telerik.com/devtools/justmock/basic-usage/arrange-act-assert 2 3 4 5

  3. https://automationpanda.com/2020/07/07/arrange-act-assert-a-pattern-for-writing-good-tests/

  4. https://codefresh.io/learn/unit-testing/

  5. https://green1229.tistory.com/159 2 3 4

  6. https://brunch.co.kr/@tilltue/55 2 3 4

  7. https://giron.tistory.com/104 2

  8. https://circleci.com/blog/how-to-test-software-part-i-mocking-stubbing-and-contract-testing/ 2

  9. https://flambeeyoga.tistory.com/entry/Test-Double과-Stub-Mock-그리고-Fake 2 3 4

  10. https://www.getxray.app/blog/maximize-your-coverage-with-test-paramtererization-and-data-driven-testing 2 3 4

  11. https://en.wikipedia.org/wiki/Data-driven_testing 2 3

  12. https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html 2

  13. https://www.browserstack.com/guide/jest-parameterized-test

  14. https://www.getxray.app/blog/test-parameterization-techniques 2 3 4

  15. https://www.linkedin.com/pulse/data-driven-testing-parameterization-tools-elise-lowry 2 3

  16. https://www.aiotests.com/blog/what-is-parameterization-in-testing

  17. https://docs.qase.io/general/get-started-with-the-qase-platform/test-cases/test-case-parameters 2

  18. https://www.zoho.com/qengine/know/data-driven-testing.html 2

  19. https://jazzteam.org/technical-articles/data-driven-testing/

  20. https://smartbear.com/blog/your-guide-to-data-driven-testing/