ユニットテストツール「Jest」の使い方

Facebookがオープンソースとして開発しているJestを利用して、ユニットテスト行う方法について確認します。導入方法、主なMatcher、モックの利用方法など取り上げます。

Jestとは

Facebookがオープンソースとして開発している ユニットテストツール です。

ユニットテストツールはJest以外にも以下のようなツールが存在します。

分類 概要
テストランナー テスト実行環境、検証結果のレポート機能などを提供 Karma
テストフレームワーク describe it などテストの構造を作る機能を提供 Mocha
アサーション テスト結果が期待通りであるか判定する機能を提供 Chai
テストユーティリティ モック、スタブなどの機能を提供 Sinon

Jestは上記機能をオールインワンで提供しているので、導入の負担が低いのが魅力です。

簡単なテストで動作確認

まず、簡単なテストをJestで実行させるところまで確認します。

プロジェクト作成

パッケージ管理に yarn を利用します。 yarn initpackage.json を作成します。

$ yarn init
$ cat package.json
{
  "name": "test1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

Jestをインストール

$ yarn add --dev jest

Jestがインストールされました。

$ 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関数
    • 引数にテスト対象の処理を記述
    • マッチャー( toBe toEqual など)で期待する動作を検証
「test関数」と「it関数」
ここではtest関数を利用しましたが、it関数でも同じ動作をします。
「describe関数」でテスト対象をカテゴライズ
ここでは利用しませんでしたが、describe関数を利用すると複数のテストをグルーピングできます。テストのカテゴライズができるので、テストの管理に役立ちます。

https://jestjs.io/docs/en/api#describename-fn

ファイル構成

ここまでの作業で、以下のようなファイル構成になりました。

.
├── node_modules/
├── package.json
├── sum.js
├── sum.test.js
└── yarn.lock

テスト実行

yarnコマンド 経由でテストを実行できるように、package.jsonscripts を以下のように記述します。

$ 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
525-javascript-jest_coverage.png

watch, watchAll

525-javascript-jest_watch.gif

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$/);
})

下記ページでより詳しい使い方を確認できます。

モックの利用方法

モックの利用方法を紹介します。

関数のモック

関数をモックに差し替えてみます。

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()

詳しい利用方法は以下ページで確認できます。

設定調整

調整方法

以下の方法で設定を調整できます。

  • 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

参考