Jest는 대표적인 자바스크립트 테스트 프레임워크이다. 하고 있는 프로젝트에 테스트 코드를 작성하고 싶어서 Jest 공식 문서를 공부하고 정리해봤다.
환경 구축
설치
npm install --save-dev jest @types/jest
@types/jest는 주로 타입스크립트에서 jest api를 import하지 않고 바로 사용하기 위해 쓰는데
나는 이걸 설치하니 jest api 메소드의 타입 힌트를 볼 수 있어서 설치했다.
babel과 함께 사용한다면 babel-jest 설치
npm install --save-dev babel-jest
babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
ESLint 사용 시
.eslintrc.js
{
"overrides": [
{
"files": ["tests/**/*"],
"env": {
"jest": true
}
}
]
}
package.json 테스트 설정
{
"scripts": {
"test": "jest"
}
}
alias 적용하기 (절대 경로를 사용하고 있는 경우)
module.exports = {
moduleNameMapper: {
'@components/(.*)': '<rootDir>/src/components/$1',
},
transformIgnorePatterns: ['<rootDir>/node_modules/']
};
라이브러리를 사용한 코드를 테스트 할 경우 라이브러리 import를 위해 transformIgnorePatterns 설정이 필요했다.
테스트 코드 작성하기
예시
sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test(name, fn, timeout)
- it(name, fn, timeout)를 별칭으로 사용할 수 있다.
첫 번째 인수는 테스트 이름, 두 번째 인수는 테스트할 기대치를 포함하는 함수이다. 세 번째 인수(선택 사항)는 중단하기 전에 대기할 시간을 지정하는 timeout (in milliseconds)이다. 기본 timeout 값은 5초이다.
테스트에서 Promise가 반환되면 Jest는 테스트가 완료되기 전에 Promise가 resolve될 때까지 기다린다.
값이 특정 조건을 충족하는지 확인하기 위해 expect를 사용할 수 있다.
expect는 다양한 항목을 검증할 수 있는 여러 matcher에 접근할 수 있게 한다.
Matcher
공통
- toBe : 값이 일치하는지 비교
- toEqual : 객체 또는 배열의 값 비교 (재귀적으로 모든 필드를 비교한다.)
not을 사용하여 matcher의 반대를 테스트할 수 있다.
진위
- toBeNull
- toBeUndefined
- toBeDefined
- toBeTruthy
- toBeFalsy
숫자
- toBeGreaterThan
- toBeGreaterThanOrEqual
- toBeLessThan
- toBeLessThanOrEqual
- toBeCloseTo : 부동 소수점 비교를 위해서 toEqual 대신 사용해야 한다.
문자열
- toMatch
배열 또는 이터러블
- toContain
- toHaveLength(number)
예외
- toThrow
함수 호출
- toHaveBeenCalled
- toHaveBeenCalledWith
이 외에도 다양한 matcher가 있다.
비동기 코드 테스트하기
테스트가 promise를 반환하면 Jest는 promise가 resolve되길 기다린다. promise가 거부되면 테스트는 실패한다.
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Async / Await
대안으로 async, await를 테스트에 사용할 수 있다. 비동기 테스트 코드를 작성하기 위해 test에 전달된 함수의 앞에 async 키워드를 사용한다.
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (error) {
expect(error).toMatch('error');
}
});
async, await를 .resolves 또는 .rejects와 결합할 수도 있다. 이러한 경우 async와 await 키워드 프로미스(Promise) 예제에서 사용한 것과 동일한 논리에 대한 효과적인 문법적 설탕(syntactic sugar)이다.
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
* 반드시 promise를 return (또는 await) 해야 한다. return/await 문을 생략한 경우, 테스트는 promise가 resolve되거나 거부되기 전에 완료된다.
promise가 거부될 것으로 예상된다면 .catch 메소드를 사용한다. expert.assertions를 추가하여 특정 개수의 단언(assertion)이 호출되었는지 확인해야 한다. 그렇지 않으면 이행된 promise가 실패하지 않을 수 있다.
est('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(error => expect(error).toMatch('error'));
});
.resolves / .rejects
.resolves matcher를 expect 문에 사용할 수 있고, Jest는 promise가 resolve되길 기다린다. promise가 거부되면 테스트는 자동으로 실패한다.
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
반드시 단언문을 반환해야 한다. return을 생략하면 테스트는 fetchData로부터 반환된 promise가 resolve 되기 전에 완료되고, then()이 콜백 함수를 실행할 수 있다.
promise가 거부될 것으로 예상되면 .rejects matcher를 사용한다. 이는 .resolves matcher와 유사하게 작동한다. promise가 이행되면 테스트는 자동으로 실패한다.
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
Setup and Teardown (설정, 해제)
describe(name, fn)
연관된 여러 테스트를 그룹화하는 블록을 생성한다. 테스트 구조 계층이 있는 경우 블록을 중첩할 수 있다.
afterAll(fn, timeout)
파일 내에 있는 모든 테스트가 완료된 후에 함수를 실행한다. 이는 테스트 전반에 걸쳐 공유되는 일부 전역 설정 상태를 정리하려는 경우 종종 유용하다.
afterAll이 describe 블록 안에 있다면 이는 describe 블록 끝에 실행된다.
afterEach (fn, timeout)
파일의 각 테스트가 완료된 후 함수를 실행한다. 이는 각 테스트에서 생성된 일부 임시 상태를 정리하려는 경우 종종 유용하다. afterEach가 describe 블록 안에 있으면, 이 describe 블록 안에 있는 각 테스트들이 끝난 후에만 실행된다.
beforeAll(fn, timeout)
파일의 테스트가 실행되기 전에 함수를 실행한다. 이는 많은 테스트에서 사용되는 일부 전역 상태를 설정하려는 경우 종종 유용하다.
beforeAll이 describe 블록 안에 있다면 describe 블록 시작 부분에서 실행된다.
beforeEach(fn, timeout)
이 파일의 각 테스트가 실행되기 전에 함수를 실행한다. 이는 많은 테스트에서 사용되는 일부 전역 상태를 재설정하려는 경우 종종 유용하다.
beforeEach가 describe 블록 안에 있으면 describe 블록의 각 테스트에 대해 실행된다.
afterAll / afterEach / beforeAll / beforeEach 공통
함수가 promise를 반환하거나 generator인 경우, Jest는 계속 진행하기 전에 그 promise가 resolve되기를 기다린다. 중단하기 전에 대기할 시간을 지정하기 위해 timeout (in milliseconds)을 선택적으로 제공할 수 있다. 기본 timeout 값은 5초이다.
범위 지정
최상위 수준의 before*, after* hook은 파일의 모든 테스트에 적용된다. describe block 내부에 선언된 hook들은 해당 describe block 내에 있는 테스트에만 적용된다.
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
Jest는 실제 테스트를 실행하기 전에 테스트 파일 내의 모든 describe 핸들러를 실행한다. 이는 describe 블록 내부가 아닌 before*, after* 핸들러 내부에서 설정 및 해제를 수행하는 또다른 이유이다. description 블록이 완료되면 기본적으로 Jest는 컬렉션 단계(collection phase)에서 발생한 순서대로 모든 테스트를 순차적으로 실행하고 각 테스트가 완료되고 다음 단계로 넘어가기 전에 정리될 때까지 기다린다.
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* hook이 먼저 실행된다.
다음은 서로 의존하는 리소스를 설정하고 해제하는 예제이다.
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
test.only
테스트가 실패했을 경우 가장 먼저 확인해야 할 사항 중 하나는 해당 테스트가 유일하게 실행되는 경우에도 실패하는지 여부이다. 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');
});
Mock Funtions
모의 함수를 사용하면 함수의 실제 구현을 지우고, 함수 호출(및 해당 호출에서 전달된 매개변수)을 캡처하고, new를 사용하여 인스턴스화할 때 생성자 함수의 인스턴스를 캡처하고, 테스트 시점에 반환 값을 설정할 수 있게 하여 코드 간의 연결을 테스트할 수 있다.
함수를 모의하는 데 두 가지 방법이 있다. 테스트에서 사용되는 mock 함수를 생성하거나 모듈 의존성을 오버라이딩하는 manual mock을 작성하는 것이다.
mock 함수 사용하기
Mock 함수는 단순히 출력만 테스트하는 것이 아니라 다른 코드에 의해 간접적으로 호출되는 함수의 동작을 감시할 수 있게 해주기 때문 "스파이(spies)"라고도 불린다. jest.fn을 사용하여 mock 함수를 만들 수 있다. 구현이 제공되지 않은 경우, mock 함수는 호출될 때 undefined를 반환한다.
forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
forEach.test.js
const forEach = require('./forEach');
const mockCallback = jest.fn(x => 42 + x);
test('forEach mock function', () => {
forEach([0, 1], mockCallback);
// mock 함수가 두 번 호출된다.
expect(mockCallback.mock.calls).toHaveLength(2);
// mock 함수의 첫 번째 호출의 첫 번째 인수는 0이다.
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 함수의 두 번째 호출의 첫 번째 인수는 1이다.
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 함수의 첫 번째 호출의 반환값은 42이다.
expect(mockCallback.mock.results[0].value).toBe(42);
});
.mock property
- instances
- contexts
- calls
- results
- lastCall
자세한 내용 공식 문서 참고
Mock 반환 값
mock 함수를 통해 테스트 중에 테스트 값을 코드에 삽입할 수 있다.
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
mock 함수는 함수형 연속 전달 방식에서 매우 효과적이다. 이 스타일로 작성된 코드는 사용되기 직전에 테스트에 직접 값을 주입할 수 있도록 하여 실제 구성 컴포넌트의 동작을 재현하는 복잡한 stub을 만들 필요 없게 한다.
대부분의 실제 사례는 실제로 종속 구성 요소에서 모의 함수를 구하고 구성하는 것을 포함하지만 기술은 동일다. 이러한 경우 직접 테스트되지 않는 함수 내부에 로직을 구현하는 것을 피해야 한다.
mockClear
- mock 함수(mockFn)의 mock.calls, mock.instances, mock.contexts, mock.results 배열에 저장된 모든 정보를 지운다. 이는 두 개의 단언 사이에서 mock 함수의 사용 데이터를 정리할 때 유용하다.
* mockClear()는 단순히 mockFn.mock의 속성 값만 초기화하는 것이 아니라 mockFn.mock 자체를 교체한다는 것을 유의해야 한다. 따라서 mockFn.mock을 다른 변수에 할당하는 것은 피해야 하며, 임시 변수 여부와 상관없이 이를 통해 오래된 데이터에 접근하지 않도록 해야 한다.
jest.clearAllMocks()
모든 mock의 mock.calls, mock.instances, mock.contexts, mock.results 속성을 지운다. 모든 모의된 함수에 .mockClear()를 호출하는 것과 같다.
mockReset
- mockClear가 수행하는 모든 작업을 수행하고 mock 구현을 빈 함수로 대체하여 undefined를 반환한다.
jest.resetAllMocks()
모든 mock의 상태를 리셋한다. 모든 모의된 함수에 .mockReset()을 호출하는 것과 같다.
Mocking Modules
jest.mock(moduleName, factory, options)
모듈이 필요할 때 자동 버전으로 모듈을 mock 할 수 있다. factory와 options는 선택 사항이다. factory는 Jest의 자동 mocking 특징을 사용하는 대신 실행되는 명시적 모듈 factory를 지정하는 데 사용된다. auto-mocked된 경우 undefined를 반환한다.
예를 들어 API 요청이 포함된 테스트를 수행하려는 경우, API를 실제로 사용하지 않고(따라서 느리고 취약한 테스트를 생성하지 않고) jest.mock(...) 함수를 사용하여 axios 모듈을 자동으로 모의할 수 있다.
모듈을 모의하면 테스트에서 확인하고자 하는 데이터를 반환하는 .get에 대한 mockResolvedValue를 제공할 수 있다. axios.get이 가짜 응답을 반환하도록 하려는 것이다.
users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
Mocking Partials
모듈의 일부만 모의하고 나머지는 실제 구현을 유지할 수 있다. default export(기본 내보내기)와 함께 ES6 모듈의 factory 매개변수를 사용하는 경우 __esModule: true 속성을 지정해야 한다. 이 속성은 Babel / TypeScript에 의해 자동 생성되지만 여기서는 수동으로 설정해야 한다. 기본 내보내기를 가져올 때, 이는 내보내기 객체에서 default라는 속성을 가져오라는 지시다.
foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');
//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});
test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});
jest.requireActual(moduleName)
- 모듈이 mock 구현을 받아야 하는지 여부에 대한 모든 검사를 건너뛰고, mock 대신 실제 모듈을 반환한다.
jest.mock으로 모의된 모듈은 jest.mock을 호출하는 파일에 대해서만 모의된다. 모듈을 import하는 다른 파일은 모듈을 모의하는 테스트 파일 이후에 실행되더라도 원래 구현을 가져온다.
Mock 구현
반환 값을 지정하는 능력을 넘어서 모의 함수의 구현을 완전히 대체하는 것이 유용한 경우가 있다. 이는 jest.fn 또는 모의 함수의 메서드 mockImplementationOnce로 수행할 수 있다.
mockImplementationOnce 메소드는 다른 모듈에서 생성된 모의 함수의 기본 구현을 정의해야 할 때 유용하다.
여러 함수 호출이 서로 다른 결과를 생성하는 모의 함수의 복잡한 동작을 재생성해야 하는 경우 mockImplementationOnce 메서드를 사용한다.
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false
모의 함수가 mockImplementationOnce로 정의된 구현을 모두 실행하면, jest.fn으로 설정된 기본 구현을 실행한다. (정의되어 있다면)
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
자세한 내용은 공식 문서 참고
Mock Names
Mock Names를 통해 mock 함수에 이름을 설정하면 테스트 오류 출력에서 jest.fn() 대신 해당 이름을 확인할 수 있다. 오류를 보고하는 mock 함수를 빠르게 식별하고 싶을 때 사용하자.
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
jest.spyOn(object, methodName)
jest.fn과 유사한 mock 함수를 생성하지만 object[methodName] 호출도 추적한다. Jest mock 함수를 반환한다.
jest.spyOn이 mock(모의 객체)이기 때문에 afterEach hook에 전달되는 콜백 함수의 body에 jest.restoreAllMocks를 호출하여 초기 상태를 복원할 수 있다.
video.js
const video = {
play() {
return true;
},
};
module.exports = video;
play.test.js
const video = require('./video');
afterEach(() => {
// spyOn으로 생성된 spy 복원
jest.restoreAllMocks();
});
test('plays video', () => {
const spy = jest.spyOn(video, 'play');
const isPlaying = video.play();
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
});
mockFn.mockRestore()
mockFn.mockReset()가 수행하는 것을 수행하고 원래(모의되지 않은) 구현을 복원한다. 이는 특정 테스트 케이스에서 함수를 모의하고 다른 케이스에선 원래 구현을 복원하려고 할 때 유용하다. 각 테스트 전에 mock을 자동으로 복원하려면 Jest restoreMocks 구성 옵션을 사용하면 된다.
mockFn.mockRestore()는 오직 mock이 jest.spyOn()으로 생성되었을 때만 동작한다. 따라서 jest.fn()을 수동 할당할 때는 복원 작업을 직접 처리해야 한다.
jest.restoreAllMocks()
모든 mock과 대체된 속성을 원래 값으로 복원한다. 모든 모의 함수에 .mockRestore()를 호출하고 모든 대체된 속성에 .restore()를 호출하는 것과 같다. jest.restoreAllMocks()는 jest.spyOn()으로 생성된 mock과 jest.replaceProperty()로 대체된 속성에 대해서만 작동한다. 다른 mock은 수동으로 복원해야 한다.
기타 API
Jest Object
Fake Timer / Misc
- jest.useFakeTimers(fakeTimersConfig?)
- jest.runAllTimers()
- jest.clearAllTimers()
- jest.now()
- jest.setTimeout(timeout)
더 많은 메소드는 공식 문서 참고
테스트 환경 설정
testEnvironment [string]
Jest의 기본 환경은 Node.js 환경이다. 웹앱을 빌드하는 경우 jsdom을 통해 브라우저와 유사한 환경을 사용할 수 있다.
파일 맨 위에 @jest-environment docblock을 추가하면 해당 파일에 있는 모든 테스트에 사용할 다른 환경을 지정할 수 있다.
/**
* @jest-environment jsdom
*/
test('use jsdom in this test file', () => {
const element = document.createElement('div');
expect(element).not.toBeNull();
});
'Develop > JavaScript' 카테고리의 다른 글
[JavaScript] 카카오맵 API를 이용한 장소 검색 및 위치 추가 구현하기 (0) | 2024.03.19 |
---|---|
Vanilla JavaScript 프로젝트 초기 설정하기 (ES6) (0) | 2024.02.06 |
[JavaScript] localStorage 사용하기 (0) | 2023.02.22 |
[JavaScript] Destructuring Assignment (구조 분해 할당) (0) | 2023.02.03 |
[JavaScript] ?.(Optional Chaining) / ??(Nullish Coalescing Operator) (0) | 2023.02.02 |