ユニットテストツール「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

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

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

モックの利用方法

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

関数のモック

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

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

参考

よかったらシェアしてね!
目次