Writing tests with 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 Woongjae Lee

Writing tests with JEST

타입스크립트 한국 유저 그룹 리액트 스터디 201706

  • 1,132