테스트 셋업과 정리

테스트 셋업과 정리 (Setup and Teardown)

테스트를 작성하다 보면 테스트가 실행되기 전에 해야 할 준비 작업이 있고, 실행된 뒤에 해야 할 마무리 작업이 있어요. Jest는 이런 반복적인 준비·정리 흐름을 잡아주는 헬퍼 함수를 제공해요. 이 글에서는 beforeEach/afterEach, beforeAll/afterAll 그리고 스코프 규칙과 실행 순서를 다룰게요.

출처: Jest 공식 문서 — Setup and Teardown

반복 셋업 (Repeating Setup)

여러 테스트에서 반복적으로 해야 할 작업이 있다면 beforeEachafterEach 훅을 사용하면 돼요.

예를 들어 여러 테스트가 도시 데이터베이스를 다룬다고 해 볼게요. 각 테스트 전에 호출해야 하는 initializeCityDatabase() 메서드가 있고, 각 테스트 후에 호출해야 하는 clearCityDatabase() 메서드가 있어요. 이렇게 처리할 수 있죠.

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

beforeEachafterEach테스트가 비동기 코드를 다루는 것과 같은 방식으로 비동기 코드를 처리할 수 있어요. done 파라미터를 받거나 Promise를 반환하면 되죠. 예를 들어 initializeCityDatabase()가 데이터베이스가 초기화되면 resolve되는 Promise를 반환한다면, 그 Promise를 반환하면 돼요.

beforeEach(() => {
  return initializeCityDatabase();
});

일회성 셋업 (One-Time Setup)

어떤 경우에는 파일의 시작 부분에서 한 번만 셋업하면 충분해요. 특히 셋업이 비동기여서 인라인으로 할 수 없을 때 그렇죠. Jest는 이런 상황을 위한 beforeAllafterAll 훅을 제공해요.

예를 들어 initializeCityDatabase()clearCityDatabase()가 둘 다 Promise를 반환하고, 도시 데이터베이스를 테스트 사이에 재사용할 수 있다면, 테스트 코드를 이렇게 바꿀 수 있어요.

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

스코프 (Scoping)

최상위 레벨의 before*/after* 훅은 파일 안의 모든 테스트에 적용돼요. 반면 describe 블록 안에서 선언한 훅은 그 describe 블록 안의 테스트에만 적용되죠.

예를 들어 도시 데이터베이스뿐 아니라 음식 데이터베이스도 있다고 해 볼게요. 테스트마다 다른 셋업을 할 수 있어요.

// Applies to all tests in this file
beforeEach(() => {
  return initializeCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

describe('matching cities to foods', () => {
  // Applies only to tests in this describe block
  beforeEach(() => {
    return initializeFoodDatabase();
  });

  test('Vienna <3 veal', () => {
    expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
  });

  test('San Juan <3 plantains', () => {
    expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
  });
});

최상위 beforeEachdescribe 블록 안의 beforeEach보다 먼저 실행된다는 점을 기억하세요. 모든 훅의 실행 순서를 그림으로 보면 이렇게 돼요.

beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));

test('', () => console.log('1 - test'));

describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));

  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

실행 순서 (Order of Execution)

Jest는 실제 테스트를 실행하기 전에, 테스트 파일의 모든 describe 핸들러를 먼저 실행해요. 이것이 셋업·정리를 describe 블록 안이 아니라 before*/after* 핸들러 안에서 해야 하는 또 하나의 이유예요. describe 블록이 모두 끝나면, 기본적으로 Jest는 수집 단계에서 만난 순서대로 모든 테스트를 직렬로 실행하고, 각 테스트가 끝나 정리되기를 기다린 뒤 다음으로 넘어가요.

아래 예시 테스트 파일과 출력을 보면 이해가 쉬워요.

describe('describe outer', () => {
  console.log('describe outer-a');

  describe('describe inner 1', () => {
    console.log('describe inner 1');

    test('test 1', () => console.log('test 1'));
  });

  console.log('describe outer-b');

  test('test 2', () => console.log('test 2'));

  describe('describe inner 2', () => {
    console.log('describe inner 2');

    test('test 3', () => console.log('test 3'));
  });

  console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3

describetest 블록처럼 Jest는 before*/after* 훅도 선언된 순서대로 호출해요. 다만 바깥 스코프의 after* 훅이 먼저 호출된다는 점을 기억하세요. 예를 들어 서로 의존하는 리소스를 셋업·정리할 때는 이렇게 써요.

beforeEach(() => console.log('connection setup'));
beforeEach(() => console.log('database setup'));

afterEach(() => console.log('database teardown'));
afterEach(() => console.log('connection teardown'));

test('test 1', () => console.log('test 1'));

describe('extra', () => {
  beforeEach(() => console.log('extra database setup'));
  afterEach(() => console.log('extra database teardown'));

  test('test 2', () => console.log('test 2'));
});

// connection setup
// database setup
// test 1
// database teardown
// connection teardown

// connection setup
// database setup
// extra database setup
// test 2
// extra database teardown
// database teardown
// connection teardown

:::note

jasmine2 테스트 러너를 쓴다면, 그 러너는 after* 훅을 선언의 역순으로 호출한다는 점을 고려하세요. 위 예제와 같은 출력을 얻으려면 아래처럼 바꿔야 해요.

  beforeEach(() => console.log('connection setup'));
+ afterEach(() => console.log('connection teardown'));

  beforeEach(() => console.log('database setup'));
+ afterEach(() => console.log('database teardown'));

- afterEach(() => console.log('database teardown'));
- afterEach(() => console.log('connection teardown'));

  // ...

:::

일반적인 조언 (General Advice)

테스트가 실패하면 가장 먼저 확인할 것 중 하나는 그 테스트만 단독으로 실행했을 때도 실패하는지예요. Jest로 테스트 하나만 실행하려면, 그 test를 임시로 test.only로 바꾸면 돼요.

test.only('this will be the only test that runs', () => {
  expect(true).toBe(false);
});

test('this test will not run', () => {
  expect('A').toBe('A');
});

더 큰 스위트의 일부로 실행하면 자주 실패하는데 단독 실행할 때는 실패하지 않는 테스트가 있다면, 다른 테스트의 무언가가 이 테스트를 방해하고 있을 가능성이 커요. 이럴 땐 beforeEach로 공유 상태를 정리하면 고칠 수 있는 경우가 많아요. 공유 상태가 수정되는지 확실하지 않다면, 데이터를 로그로 남기는 beforeEach를 시도해 볼 수도 있어요.

더 알아보기

테스트 블록 구조를 더 알고 싶다면 Using Matchers와 비동기 코드 테스트의 Mock Functions 문서를 이어서 보세요.