Writing tests with Jest
example code: https://github.com/Gloridea/react-ts-jest
Jest
- Fast and sandboxed
- Built-in code coverage reports
- Zero configuration
- Powerful mocking library
- Works with TypeScript
Setup Jest
$ create-react-app {PROJ} --scripts-version=react-scripts-ts
$ cd {PROJ}
$ yarn test
Jest with MobX (1)
$ yarn add mobx
// TodoStore.ts
export class TodoStore {
@observable
todos: Todo[] = [];
@action
addTodo(label: string) {
this.todos.push(new Todo(label));
}
}
Jest with MobX (2)
// TodoStore.test.ts
import {TodoStore} from './TodoStore';
import * as assert from 'assert';
describe('TodoStore', () => {
describe('addTodo()', () => {
it('should add a Todo', () => {
// Given
const todoStore = new TodoStore();
// When
todoStore.addTodo('hello');
// Then
assert.equal(todoStore.todos.length, 1);
});
});
});
Jest with MobX (3)
$ yarn test
FAIL src/TodoStore.test.ts
● TodoStore › addTodo() › should add Todo
TypeError: Object.defineProperty called on non-object
at Function.defineProperty (native)
at classPropertyDecorator (node_modules/mobx/lib/mobx.js:991:24)
at Object.<anonymous> (src/TodoStore.test.ts:8:22)
at process._tickCallback (internal/process/next_tick.js:109:7)
Jest with MobX (4)
$ yarn add -D ts-jest identity-obj-proxy
// fileTransformer.js
module.exports = {
process(src, filename, config, options) {
const str = JSON.stringify(path.basename(filename));
return 'module.exports = ' + str + ';';
}
};
Jest with MobX (5)
// package.json
{
"jest": {
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$"
:"<rootDir>/fileTransformer.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(tsx?|jsx?)$",
"moduleNameMapper": {
"^.+\\.css$": "identity-obj-proxy"
},
"moduleFileExtensions": ["ts", "tsx", "js", "json", "jsx"]
},
"globals": {
"ts-jest": {
"skipBabel": true
}
}
}
Jest with MobX (6)
$ yarn add -D jest
// package.json
{
"test": "jest",
"test:watch": "jest --watch"
}
Jest directory structure convention
__tests__
- 테스트 코드는 __tests__ 에 두는 걸 권장
├── TodoInput.tsx
├── TodoList.css
├── TodoList.tsx
└── __tests__
└── TodoList.test.tsx
__snapshots__
- Jest 가 자동 생성한 스냅샷이 저장되는 위치
└── __tests__
├── TodoList.test.tsx
└── __snapshots__
└── TodoList.test.tsx.snap
__mocks__
- Mock 객체를 두는 위치
└── stores
├── TodoStore.ts
├── __mocks__
│ └── TodoStore.ts
└── __tests__
└── TodoStore.test.ts
Testing
React components
Write a react component
// TodoList.tsx
@observer
export class TodoList extends React.Component<{todoStore: TodoStore}, {}> {
render() {
const list = this.props.todoStore.todos.map(todo => (
<li key={todo.id}>
{todo.label}
</li>
));
return (
<ul className="TodoList">{list}</ul>
);
}
}
anyway, how to test components?
Traditional way
- jsdom을 이용해서 생성된 컴포넌트를 조사
- DOM API등을 사용해서 내부를 탐지
- enzyme등의 도움을 받을 수도 있음
- 여전히 사용 가능한 방법이지만, 일부 상태 변경 시 테스트 코드를 직접 일괄 업데이트 해야 하는 수고 존재
Snapshots
- 생성된 컴포넌트를 readable 하고 diff 가능한 text format으로 저장
- 이후 다음 테스트 시 저장된 snapshot과 비교해서 테스트 성공 여부를 판단
- Cons: The intent of the test does not reveal well.
Snapshots
// __tests__/__snapshots__/TodoList.test.tsx.snap
exports[`TodoList renders without crashing 1`] = `
<ul
className="TodoList"
>
<li>
hello
</li>
</ul>
`;
- Jest CLI 에서 바로 snapshot을 갱신할 수 있음
Shallow vs Deep
// __tests__/__snapshots__/TodoList.test.tsx.snap
exports[`TodoList renders without crashing 1`] = `
<ul
className="TodoList"
>
<li>
hello
</li>
</ul>
`;
- Jest CLI 에서 바로 snapshot을 갱신할 수 있음 (u key)
Replace react-test-renderer
with enzyme
- react-test-renderer는 snapshot 전용에 가까움 (기능이 거의 없음)
- input 테스트 필요 시 테스트를 분리해서 ReactDOM을 사용해야 함
- enzyme은 두 가지를 한꺼번에 수행 가능
test with enzyme
$ yarn add -D enzyme enzyme-to-json \
@types/enzyme @types/enzyme-to-json
// TodoInput.test.tsx
describe('TodoInput', () => {
it('should add todo', () => {
// Given
const todoStore = new TodoStore();
const wrapper = mount(<TodoInput todoStore={todoStore}/>);
// When
const input = wrapper.find('input').first();
const button = wrapper.find('button').first();
input.simulate('change', {target: {value: 'hello world'}});
button.simulate('click');
// Then
expect(todoStore.todos.length).toBe(1);
expect((input.getDOMNode() as HTMLInputElement).value).toBe('');
});
});
Test Coverage Report
Creating coverage report
$ jest --coverage
$ open coverage/index.html
// package.json
{
"jest": {
"coverageReporters": ["text", "html"]
}
}
WTH???
Coverage Types
- Line Coverage - 전체 코드 라인 중 실행된 적 있는 라인 수
- Function Coverage - 전체 함수 중 실행된 적 있는 함수 수
- Branch Coverage - 코드 내 전체 분기 중 실행된 경로 분기 수
- Statement Coverage - 코드 내 모든 문장 중 실행된 문장 수
Creating coverage report
$ jest --coverage
$ open coverage/index.html
// package.json
{
"jest": {
"mapCoverage": true
}
}
How much coverage is enough?
- 테스트 커버리지는 조작하기 쉬움
- 관리 지표로 사용하기에는 부적절
- 자율적 개선 지표로 사용할 때는 도움이 됨
- 일반적으로 7~80%을 기준으로 삼을 수 있음
- TDD 방식으로 코딩하면 자연스럽게 높은 커버리지 달성 가능
Mocks
Mock?
- 테스트 대상 코드는 일반적으로 다수의 의존 객체를 가지고 있음
- 의존은 또다른 의존 객체에 엮여있는 경우가 대부분
- 의존 객체로 인해
- 테스트가 불가능해지거나 (플랫폼 종속)
- 속도가 매우 느려지거나 (원격 API통신)
- 단위 테스트는 가급적 의존 객체의 영향을 배제하고 "자신이 작성한 코드"의 검증에 집중하는 편이 좋음
- 따라서 진짜 의존 객체 대신 가짜 의존 객체를 만들어서 사용
__mocks__
- Mock files to load from tests automatically
└── stores
├── TodoStore.ts
├── __mocks__
│ └── TodoStore.ts
└── __tests__
└── TodoStore.test.ts
__mocks__
- Mock 객체로 쉽게 대체하기 위해 Jest는 __mocks__ 라는 관례를 제공
import * as React from 'react';
import {TodoList} from '../TodoList';
import {TodoStore} from '../../stores/TodoStore';
import {mount} from 'enzyme';
jest.mock('../../stores/TodoStore');
describe('TodoList', () => {
it('should contain added todo item', () => {
const todoStore = new TodoStore(); // mock object
// additional test codes...
});
});
jest.useFakeTimers()
- 시간을 원하는 시간으로 설정하거나 원하는 만큼 흘려보낼 때 사용
// timerGames.test.ts
test('waits 1 second before ending the game', async () => {
// Given
jest.useFakeTimers();
const callback = jest.fn();
timerGame(callback);
expect(callback).not.toBeCalled();
// When
jest.runAllTimers();
// Then
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);
});
Thank you
Writing tests with JEST
By gloridea
Writing tests with JEST
- 1,400