Facebookがオープンソースとして開発しているJestを利用して、ユニットテスト行う方法について確認します。導入方法、主なMatcher、モックの利用方法など取り上げます。
Jestとは
Facebookがオープンソースとして開発している ユニットテストツール です。
ユニットテストツールはJest以外にも以下のようなツールが存在します。
| 分類 | 概要 | 例 | 
|---|---|---|
| テストランナー | テスト実行環境、検証結果のレポート機能などを提供 | Karma | 
| テストフレームワーク | describe it などテストの構造を作る機能を提供 | Mocha | 
| アサーション | テスト結果が期待通りであるか判定する機能を提供 | Chai | 
| テストユーティリティ | モック、スタブなどの機能を提供 | Sinon | 
Jestは上記機能をオールインワンで提供しているので、導入の負担が低いのが魅力です。
簡単なテストで動作確認
まず、簡単なテストをJestで実行させるところまで確認します。
プロジェクト作成
パッケージ管理に yarn を利用します。 yarn init で package.json を作成します。
$ yarn init$ cat package.json
{
  "name": "test1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}Jestをインストール
$ yarn add --dev jestJestがインストールされました。
$ cat package.json
{
  "name": "test1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^23.6.0"
  }
}テスト対象ファイル実装
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関数- 第1引数にテストの概要を記述
 - 第2引数にテストを記述
 
expect関数- 引数にテスト対象の処理を記述
 - マッチャー( 
toBetoEqualなど)で期待する動作を検証 
ここではtest関数を利用しましたが、it関数でも同じ動作をします。
ここでは利用しませんでしたが、describe関数を利用すると複数のテストをグルーピングできます。テストのカテゴライズができるので、テストの管理に役立ちます。
ファイル構成
ここまでの作業で、以下のようなファイル構成になりました。
.
├── node_modules/
├── package.json
├── sum.js
├── sum.test.js
└── yarn.lockテスト実行
yarnコマンド 経由でテストを実行できるように、package.json の scripts を以下のように記述します。
$ cat package.json
{
  "name": "test1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^23.6.0"
  },
  "scripts": {
    "test": "jest"
  }
}テストを実行します。
$ yarn test
yarn run v1.9.4
$ jest
 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (6ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.227s
Ran all test suites.
✨  Done in 3.50s.簡単なテストではありましたが、Jestを利用してテストを実行することができました。
Jestコマンドのオプション
coverage
coverageオプション を利用するとテストカバレッジを確認できます。
$ yarn test --coverage
yarn run v1.9.4
$ jest --coverage
 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (12ms)
----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 sum.js   |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.319s
Ran all test suites.
✨  Done in 3.59s.coverageオプション を利用した場合、テストを実行すると coverageフォルダ が生成されます。
coverage/lcov-report/index.html をブラウザで開くと詳しいカバレッジ情報を確認できます。
$ open coverage/lcov-report/index.html
watch, watchAll

watchオプション watchAllオプション を利用すると、ファイルの変更を監視してテストを実行してくれます。
その他
下記ページにてコマンドオプションについて確認できます。
https://jestjs.io/docs/en/cli
主なMatcher
利用頻度の高いMatcherを紹介します。
it('matchers', () => {
    // 等しい
    expect(1 + 5).toBe(6)
    // notで「~でない」の判定ができます。
    expect(1 + 5).not.toBe(7)
    // toBeは厳密な等価性を判定するので、オブジェクトの場合はtoEqualを利用します。
    expect({ a: 100 }).toEqual({ a: 100 })
    // 大きい
    expect(4).toBeGreaterThan(3)
    expect(4).toBeGreaterThanOrEqual(4)
    // 小さい
    expect(4).toBeLessThan(5)
    expect(4).toBeLessThanOrEqual(4)
    // null, true, false
    expect(null).toBeNull()
    expect(true).toBeTruthy()
    expect(false).toBeFalsy()
    // 文字列
    expect('abcdefg').toMatch(/bc/)
    expect('abcdefg').not.toMatch(/bd/)
    // 配列
    expect([1, 2, 3]).toContain(2)
    // 例外
    const xxx = () => {
        throw new Error('error message xxx');
    }
    expect(xxx).toThrow('error message xxx');
    expect(xxx).toThrow(/.*xxx$/);
})下記ページでより詳しい使い方を確認できます。
- 代表的なものだけ確認
 - 全てのMatcherを確認
 
モックの利用方法
モックの利用方法を紹介します。
関数のモック
関数をモックに差し替えてみます。
const myMethod = (cnt, callback) => {
    let total
    while (cnt) {
        total = callback(cnt)
        cnt--
    }
    return total
}
it('mock', () => {
    // arrange
    const mockCallback = jest.fn(x => x * 2)
    // act
    myMethod(3, mockCallback)
    // assert
    // モックメソッドが3回呼ばれたこと
    expect(mockCallback.mock.calls.length).toBe(3)
    // モックメソッドが受け取った引数
    expect(mockCallback.mock.calls[0][0]).toBe(3)  // 1回目 第1引数
    expect(mockCallback.mock.calls[1][0]).toBe(2)  // 2回目 第1引数
    expect(mockCallback.mock.calls[2][0]).toBe(1)  // 3回目 第1引数
    // モックメソッドの戻り値
    expect(mockCallback.mock.results[0].value).toBe(6)  // 1回目
    expect(mockCallback.mock.results[1].value).toBe(4)  // 2回目
    expect(mockCallback.mock.results[2].value).toBe(2)  // 3回目
})クラスのモック
ClassAとClassBを用意します。
export default class ClassA {
  sum(x, y) {
    return x + y
  }
}import ClassA from './class-a'
export default class ClassB {
  constructor() {
    this.classA = new ClassA()
  }
  total(x, y, z) {
    return this.classA.sum(x, y) * z
  }
}ClassAをモックに差し替えてみます。
import ClassA from './class-a'
import ClassB from './class-b'
// 自動モック
jest.mock('./class-a')
describe('ClassA', () => {
  beforeEach(() => {
    ClassA.mockClear()
  })
  it('constructor()', () => {
    expect(ClassA).not.toHaveBeenCalled()
    new ClassB()
    expect(ClassA).toHaveBeenCalledTimes(1)
  })
  describe('total()', () => {
    it('mockReturnValue', () => {
      // Arrange
      ClassA.prototype.sum = jest.fn().mockReturnValue(10)
      const classB = new ClassB()
      // Act
      const result = classB.total(1, 2, 3)
      // Assert
      expect(result).toBe(30)  // 10[mockReturnValueの引数] * 3
      expect(ClassA.prototype.sum).toHaveBeenCalledTimes(1)
    })
    it('mockImplementationOnce', () => {
      // Arrange
      ClassA.prototype.sum = jest.fn().mockImplementationOnce((x, y) => {
        expect(x).toBe(1)
        expect(y).toBe(2)
        return 100
      })
      const classB = new ClassB()
      // Act
      const result = classB.total(1, 2, 3)
      // Assert
      expect(result).toBe(300) // 100[mockImplementationOnceの戻り値] * 3
      expect(ClassA.prototype.sum).toHaveBeenCalledWith(1, 2)
    })
  })
})モック関連で利用する主なメソッド
jest.fn()- mock functionを生成
 
jest.mock()- モジュール、クラスの自動モック
 
jest.fn().mockReturnValue()- 呼ばれたときに代わりに返す値を設定
 
jest.fn().mockImplementation()- 呼ばれたとき代わりに実行させる処理を設定
 
- Matchers
- 呼ばれたことを検証
toHaveBeenCalled()toHaveBeenCalledTimes()toHaveBeenCalledWith()
 
 - 呼ばれたことを検証
 
詳しい利用方法は以下ページで確認できます。
- https://jestjs.io/docs/en/mock-functions
 - モジュールのモック
- https://jestjs.io/docs/en/mock-functions#mocking-modules
 - axiosのAPI呼び出しをモックに差し替えるなどの利用方法が解説されています。
 
 - クラスのモック
 
設定調整
調整方法
以下の方法で設定を調整できます。
package.jsonファイルに設定を記述jest.config.jsファイルを作成して、設定を記述- テスト実行時にオプションで設定を調整
 
詳しい設定方法などは以下ページで確認できます。
https://jestjs.io/docs/en/configuration
testRegexでテストファイルを調整
Jestはデフォルトだと以下のファイルをテストファイルとみなします。
*.test.js*.spec.js__tests__ディレクトリ以下のファイル
変更したい場合は、 testRegex の設定を調整します。
https://jestjs.io/docs/en/configuration#testregex-string