테스트 셋업과 정리
테스트 셋업과 정리 (Setup and Teardown)
테스트를 작성하다 보면 테스트가 실행되기 전에 해야 할 준비 작업이 있고, 실행된 뒤에 해야 할 마무리 작업이 있어요. Jest는 이런 반복적인 준비·정리 흐름을 잡아주는 헬퍼 함수를 제공해요. 이 글에서는 beforeEach/afterEach, beforeAll/afterAll 그리고 스코프 규칙과 실행 순서를 다룰게요.
반복 셋업 (Repeating Setup)
여러 테스트에서 반복적으로 해야 할 작업이 있다면 beforeEach와 afterEach 훅을 사용하면 돼요.
예를 들어 여러 테스트가 도시 데이터베이스를 다룬다고 해 볼게요. 각 테스트 전에 호출해야 하는 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();
});
beforeEach와 afterEach는 테스트가 비동기 코드를 다루는 것과 같은 방식으로 비동기 코드를 처리할 수 있어요. done 파라미터를 받거나 Promise를 반환하면 되죠. 예를 들어 initializeCityDatabase()가 데이터베이스가 초기화되면 resolve되는 Promise를 반환한다면, 그 Promise를 반환하면 돼요.
beforeEach(() => {
return initializeCityDatabase();
});
일회성 셋업 (One-Time Setup)
어떤 경우에는 파일의 시작 부분에서 한 번만 셋업하면 충분해요. 특히 셋업이 비동기여서 인라인으로 할 수 없을 때 그렇죠. Jest는 이런 상황을 위한 beforeAll과 afterAll 훅을 제공해요.
예를 들어 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);
});
});
최상위 beforeEach는 describe 블록 안의 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
describe와 test 블록처럼 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 문서를 이어서 보세요.