test
테스트 할 함수를 받고 테스트를 수행하는 하나의 단위
test('should work as expected', () => {
expect(Math.sqrt(4)).toBe(2)
})
test.skip
특정 테스트가 실행되는 것을 스킵
test.skip('skipped test', () => {
// 스킵 되고 테스트 에러가 발생하지 않습니다.
assert.equal(Math.sqrt(4), 3)
})
test.only
특정 테스트만 실행
test.only('test', () => {
// 다른 테스트가 있어도 이 테스트만 실행됩니다.
assert.equal(Math.sqrt(4), 2)
})
test.runIf
특정 환경에서만 테스트를 실행
const isDev = process.env.NODE_ENV === 'development'
test.runIf(isDev)('dev only test', () => {
// 개발환경에서만 실행됩니다.
})
test.concurrent
테스트를 병렬로 실행
describe('suite', () => {
test('serial test', async () => { /* ... */ })
// concurrent test 1, 2가 동시에 실행됩니다.
test.concurrent('concurrent test 1', async () => { /* ... */ })
test.concurrent('concurrent test 2', async () => { /* ... */ })
})
test.sequential
테스트를 순차적으로 표시
describe.concurrent('suite', () => {
test('concurrent test 1', async () => { /* ... */ })
test('concurrent test 2', async () => { /* ... */ })
// 병렬로 동작하지만, 아래 두개는 순차적으로 표시되는 것이 보장됩니다.
test.sequential('sequential test 1', async () => { /* ... */ })
test.sequential('sequential test 2', async () => { /* ... */ })
})
test.todo
테스트 구현은 아직 안 했지만 보고서에 표시하기 위해 사용
test.todo('테스트 구현 예정')
describe
테스트를 하나의 그룹으로 모을 때 사용
describe('sort', () => {
test('오름차순 정렬', () => {
...
});
bench('5개 요소', () => {
...
});
});
describe.skip
특정 테스트 그룹이 테스트 실행되는 것을 스킵
describe.skip('실행되지 않는 테스트', () => {
...
})
describe.only
특정 테스트 그룹만 테스트 실행
describe.only('이것만 실행되는 테스트', () => {
...
});
beforeEach
각각 테스트가 실행 되기 전에 호출되는 훅
beforeEach(async () => {
// 예를 들어 모든 테스트에서 필요한 객체를 초기화 한다거나 공통 준비과정을 가집니다.
await prepareModules();
});
afterEach
각각 테스트가 실행된 후에 호출되는 훅
afterEach(async () => {
// 예를 들어 모든 테스트에서 필요한 객체를 해제 한다거나 공통 준비과정을 가집니다.
await destroyModules()
});
beforeAll
모든 테스트가 실행되기 전에 호출되는 훅
beforeAll(async () => {
// mocking 서버를 실행 합니다.
await startMockingServer();
});
afterAll
모든 테스트가 실행된 후에 호출되는 훅
beforeAll(async () => {
// mocking 서버를 정지합니다.
await stopMockingServer();
});
Assertion이란 기대한 결과와 실제 결과가 같은지 확인하는 문장
function add(a, b) {
return a + b;
}
test("2 + 3 = 5", () => {
expect(add(2, 3)).toBe(5); // 여기가 assertion
});
기대값과 비교 하기위해 객체를 생성
expect(input).toBe(2); // expect로 생성한 후 기대값 '2'와 같은지 검증 합니다.
=== 비교
const count = 13;
test('count는 13입니다.', () => {
expect(count).toBe(13)
})
const count = 11;
test('count는 13이 아닙니다', () => {
expect(count).not.toBe(13)
})
toBe 처럼 === 비교지만, 재귀적으로 깊은 비교를 진행
const stock = {
type: 'apples',
count: 13,
}
test('사과 재고가 13개 있습니다.', () => {
// 이 검증은 성공합니다.
expect(stock).toEqual({ type: 'apples', count: 13 });
// 이 검증은 실패합니다.
expect(stock).toBe({ type: 'apples', count: 13 });
});
기본적으로 toEqual과 동일하지만, 더 엄격하게 비교
대표적으로 undefined, constructor 비교
class Stock {
constructor(type) {
this.type = type;
}
}
test('structurally the same, but semantically different', () => {
// 성공 합니다.
expect(new Stock('apples')).toEqual({ type: 'apples' });
expect([,3]).toEqual([undefined, 3]);
// 실패 합니다.
expect(new Stock('apples')).toStrictEqual({ type: 'apples' });
expect([,3]).toStrictEqual([undefined, 3]);
});
‘요소’라고 볼 수 있는 대상이면 포함 여부를 검증 (배열, 문자열, DOM 요소)
test('toContain 써보기', () => {
expect('my orange').toContain('orange');
// 클래스 배열에서 문자열 확인하기
expect(document.querySelector('#el').classList).toContain('flex');
// 요소가 자식 요소인지 확인하기
expect(document.querySelector('#wrapper')).toContain(document.querySelector('#el'));
});
length 프로퍼티를 비교
test('toHaveLength', () => {
expect([1, 2, 3]).toHaveLength(3);
expect('abc').not.toHaveLength(3);
expect({ length: 3 }).toHaveLength(3);
})
프로퍼티를 갖고 있는지 비교
const invoice = {
isActive: true
};
test('John Doe Invoice', () => {
expect(invoice).toHaveProperty('isActive');
});
정규식을 이용해서 비교
test('top fruits', () => {
expect('top fruits include apple, orange and grape').toMatch(/apple/);
});```
#### toThrowError
에러가 던져지는지 확인
```js
function iThrowError() {
throw new Error("i'm not error");
}
test('throws on pineapples', () => {
// 정규식으로 에러 메세지를 검증할 수 있습니다.
expect(() => iThrowError()).toThrowError(/not/)
})
함수가 호출되었는지 확인
const iAmMock = {
doing() {/* ... */},
};
test('mock function', () => {
// mock에 대한 자세한 내용은 뒤에서 다룰 수 있습니다.
const doing = vi.spyOn(iAmMock, 'doing');
// doing은 호출되지 않았습니다.
expect(doing).not.toHaveBeenCalled();
iAmMock.doing();
// doing이 호출 되었습니다.
expect(doing).toHaveBeenCalled()
})
조건문을 통해 확인
const isOdd = (value: number) => value % 2 !== 0
it('1은 홀수입니다.', () => {
expect(1).toSatisfy(isOdd);
});
스냅샷 테스트는 어떻게 동작하나요?
toMatchSnapshot() 호출과 같은 특정 기준점에 스냅샷 파일(*.snap)로 저장합니다. 그리고 테스트할 새로운 스냅샷을 기존 스냅샷과 비교합니다. 이렇게 참조 기준점을 관리하기 쉽도록 설계되어 있습니다. 또한 u 키를 눌러 스냅샷을 쉽게 업데이트 할 수 있습니다.
RTL과 함께 사용할 경우 컴포넌트의 렌더링 결과(React Element 구조)를 스냅샷으로 저장합니다. (App.test.tsx.snap)
스냅샷을 비교
test('스냅샷 테스트', () => {
const data = { foo: new Set(['bar', 'snapshot']) }
// - 만약 테스트를 실행할 때 스냅샷이 없다면 새로운 스냅샷 파일을 생성할 것입니다.
// - 다음번에 테스트할 때 이전 스냅샷과 비교하여 다르면 에러를 던집니다.
expect(data).toMatchSnapshot();
});
파일이 아니라 인라인 문자열을 사용
test('인라인 스냅샷 테스트', () => {
const data = { foo: new Set(['bar', 'snapshot']) }
// 인라인 문자열을 이용해서 스냅샷 테스트를 수행합니다.
expect(data).toMatchInlineSnapshot(`
{
"foo": Set {
"bar",
"snapshot",
},
}
`)
})
snap 파일 대신 다른 파일을 사용
it('파일 스냅샷 테스트', async () => {
const result = renderHTML(h('div', { class: 'foo' }));
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
})
렌더링 -> 사용자 인터랙션 -> 결과 검증할 대상(UI)가져오기 -> 검증하기
실제 브라우저 엔진을 이용해서 시뮬레이션 하면 테스트 비용이 매우 크고 다양한 변수가 존재할수 있음. 그래서 RTL은 리액트 렌더링을 통한 가상 DOM 객체 이용
렌더링 수행
import {render} from '@testing-library/react';
test('Login', () => {
render(<Login />)
})
렌더링 된 화면처럼 인터페이스가 구현된 객체에 접근
import {render, screen} from '@testing-library/react';
test('should show login form', () => {
render(<Login />)
// 화면에서 Username 레이블 텍스트를 가진 요소를 가져옵니다.
const input = screen.getByLabelText('Username');
})
고려사항
1. 사용자 인터렉션 혹은 렌더링은 비동기로 동작하는 경우가 많다. 따라서 렌더링이 모두 되었는지 몇번 재시도 하거나 기다리는 동작이 필요할 수 있다.
2. ‘요소를 가져온다’는 동작이 ‘Given’일 수도 있지만, ‘Assertion’일 수도 있다.
단일 요소 가져오기
| 쿼리 유형 | 0개 일치 | 1개 일치 | 1개 이상 일치 | 재시도(비동기) |
|---|---|---|---|---|
getBy... | 오류 발생 | 요소 반환 | 오류 발생 | 아니요 |
queryBy... | null 반환 | 요소 반환 | 오류 발생 | 아니요 |
findBy... | 오류 발생 | 요소 반환 | 오류 발생 | 예 |
다중 요소 가져오기
| 쿼리 유형 | 0개 일치 | 1개 일치 | 1개 이상 일치 | 재시도(비동기) |
|---|---|---|---|---|
getAllBy... | 오류 발생 | 배열 반환 | 배열 반환 | 아니요 |
queryAllBy... | [] 반환 | 배열 반환 | 배열 반환 | 아니요 |
findAllBy... | 오류 발생 | 배열 반환 | 배열 반환 | 예 |
접근성 트리에서 role을 기반으로 가져옴
test('', () => {
// role 목록: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles
screen.getByRole('button');
});
test용 id 속성을 넣어 가져옴
test('', () => {
// data-testid="test-button" 속성을 가진 요소를 가져옵니다.
screen.getByTestId('test-button');
});
텍스트를 기반으로 가져옴
// <div>Hello World</div>
screen.getByText('Hello World') // 텍스트가 매치되는 요소를 가져옴
screen.getByText('llo Worl', {exact: false}) // 일부만 매치되도 가져옴
screen.getByText(/World/) // 정규식 활용해서 가져옴
screen.getByText((content, element) => content.startsWith('Hello')) // 함수로 가져옴
dom selector 를 이용해 직접 가져옴
const {container} = render(<MyComponent />)
const foo = container.querySelector('[data-foo="bar"]')
검증할때는 위에서 확인했던 Assertion API를 그대로 활용
비동기적으로 인터렉션이나 요소의 생성/수정/삭제 등을 기다려야 하는 상황에 유틸 사용가능.
일정 시간마다 콜백 함수를 호출해서 완료될 때 까지 기다림
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
요소가 사라질때까지 기다림
await waitForElementToBeRemoved(() => getByText(/not here/i))
1. fireEvent
DOM 이벤트를 발생시키는 메서드로, 다양한 이벤트 유형에 대한 편의 메서드를 제공.
사용자가 생성한 액션(이벤트)을 테스트하기 위해 fireEvent API를 사용.
클릭 이벤트나 입력 이벤트 등을 DOM에 직접 dispatch 하여 시뮬레이션
주의사항
사용자가 요소를 클릭할 때 클릭 이벤트만 발생하는 것은 아닙니다. mouseOver → mouseMove → mouseDown → focus → mouseUp → click 과 같이 여러 이벤트가 순차적으로 발생합니다. 하지만 fireEvent는 해당 이벤트만 발생시키기 때문에 실제 사용자 관점의 테스트가 시뮬레이션 되기 어려울 수 있습니다.
2. userEvent
fireEvent를 기반으로 만들어진 라이브러리로, 사용자가 실제로 수행할 수 있는 상호작용을 시뮬레이션.
라벨을 클릭하는 것과 같은 요소의 유형에 기반한 상호작용을 시뮬레이션
사용자의 동작과 유사하게 테스트하는것이 테스트 신뢰도를 높이기때문에 userEvent 사용을 권장. 대부분의 테스트는 userEvent로 작성이 가능하며, userEvent에서 제공되지 않는 사례를 테스트할경우 fireEvent를 사용하는게 좋음.
직접 요소와 이벤트 객체를 인자로 줘서 dispatch
fireEvent(
getByText(container, 'Submit'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
// 이런 방식도 가능합니다.
// fireEvent.click(getByText(container, 'Submit'));
이벤트 객체 생성을 돕는 유틸
createEvent('input', inputNode, {
target: {files: inputFiles}, // file을 input 트리거 시킬 수 있습니다.
...init,
})
// 이렇게도 됩니다.
// createEvent.input(inputNode, { ... })
userEvent의 세션을 시작
const user = userEvent.setup()
await user.keyboard('[ShiftLeft>]')
fireEvent와 마찬가지로 user[eventName]으로 트리거
user.click()
user.keyboard()
user.pointer()
마우스와 터치와 같이 포인터 기반 기기를 시뮬레이션
pointer({target: element, offset: 2, keys: '[MouseLeft]'})
키보드 입력을 시뮬레이션
keyboard('foo')
keyboard('{Shift}{f}{o}{o}') // Shift, f, o, o 가 입력됩니다
keyboard('{{a[[') // {, a, [가 입력됩니다. 예약 문자는 두번 입력하면 입력 됩니다.
keyboard('{\\}}') // }가 입력 됩니다. 백슬래쉬로 escape 처리할 수 있습니다.
keyboard('[ShiftLeft][KeyF][KeyO][KeyO]') // Shfit, f, o, o가 입력됩니다. []로 keyCode를 통해 입력할 수 있습니다.
클립보드로 복사
copy()
클립보드로 오려두기
cut()
클립보드에 있는 문자를 붙여넣기
past(clipboardData?)
실행을 추적할 수 있는 mock 함수로 만듦
const fn = vi.fn()
fn('hello world');
fn.mock.calls[0] === ['hello world']
객체의 메서드 또는 함수 프로퍼티를 실행을 추적할 수 있는 mock 함수로 만듦
const market = {
getApples: () => 100
}
const getApplesSpy = vi.spyOn(market, 'getApples')
market.getApples()
getApplesSpy.mock.calls.length === 1
mock 함수에 구현을 추가
const mockFn = vi.fn().mockImplementation(apples => apples + 1);
// fn에서 바로 인자로 줘서 가능합니다: vi.fn(apples => apples + 1);
const NelliesBucket = mockFn(0);
const BobsBucket = mockFn(1);
NelliesBucket === 1 // true
BobsBucket === 2 // true
mockFn.mock.calls[0][0] === 0 // true
mockFn.mock.calls[1][0] === 1 // true
mockImplem 한번만 호출
const myMockFn = vi
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
// 'first call', 'second call', 'default', 'default'
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
특정 값을 비동기적으로 resolve 하는 함수를 추가
const asyncMock = vi.fn().mockResolvedValue(42);
await asyncMock(); // 42
특정 값을 비동기적으로 reject 하는 함수를 추가
const asyncMock = vi.fn().mockRejectedValue(new Error('Async error'));
await asyncMock(); // throws "Async error"
특정 값을 리턴하는 함수를 추가
const mock = vi.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43
this 값을 리턴하는 함수를 추가
// 아래와 동일합니다.
spy.mockImplementation(function () {
return this
})
실행할 때 언떤 인자가 들어왔는지 실행 내역을 확인가능
const fn = vi.fn();
fn('arg1', 'arg2');
fn('arg3');
console.log(fn.mock.calls)
/*
[
['arg1', 'arg2'], // first call
['arg3'], // second call
];
*/
가장 최근의 실행 내역을 가져옴. 없으면 undefined
const fn = vi.fn();
fn('arg1', 'arg2');
fn('arg3');
console.log(fn.mock.lastCall)
/*
['arg3'];
*/
실행 결과에 대해 가져옴
const fn = vi.fn()
.mockReturnValueOnce('result')
.mockImplementationOnce(() => { throw new Error('thrown error') })
try { fn() } catch {}
console.log(fn.mock.results)
/*
[
{
type: 'return',
value: 'result',
},
{
type: 'throw',
value: Error,
},
]
*/
https://mswjs.io/docs/integrations/browser
npm i -D msw
// vite.comfig.js
{
...
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['vitest.setup.js'],
},
}
// vite.setup.js
import { beforeAll, afterEach, afterAll } from 'vitest';
import { serviceWorker } from './mock/workers';
beforeAll(() => {
console.log('server start');
serviceWorker.listen();
});
afterEach(() => {
serviceWorker.resetHandlers();
});
afterAll(() => {
serviceWorker.close();
});
// mock/worker.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const serviceWorker = setupServer(...handlers);
// mock/handler.js
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("http://localhost:8000/api/greet", async () => {
await new Promise(resolve => setTimeout(resolve, 300));
return HttpResponse.json({
message: 'Hello World',
})
}),
];