Jestまとめ - y-okamon/jest-sample GitHub Wiki

matcher(マッチャー)

単純な値

test('two plus two is four', () => {
  expect(2 + 2).toBe(4); // OK
});
test('string check', () => {
  expect('hoge').toBe('hoge'); // OK
});

オブジェクトや配列

toEqualは、オブジェクトまたは配列の全フィールドを再帰的にチェックする。

オブジェクトの比較

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2}); // OK
});

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  // two, one の順序を入れ替えた
  expect(data).toEqual({two: 2, one: 1}); // OK
});

配列の比較

test('array check', () => {
  const data = [1, 2, 3];
  expect(data).toEqual([1, 2, 3]); // OK
});

test('array check', () => {
  const data = [1, 2, 3];
  expect(data).toEqual([1, 2, 4]); // NG
});

反対のテスト

test('not zero', () => {
  expect(1 + 2).not.toBe(0); // OK
});

真偽値

  • toBeNullnullのみ一致する
  • toBeUndefined undefinedのみ一致する
  • toBeDefinedtoBeUndefinedと反対になる
  • toBeTruthyifステートメントで真であるとみなすものに一致する
  • toBeFalsyifステートメントで偽であるとみなすものに一致する
test('null', () => {
  const n = null;
  // all OK
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('undefined', () => {
  const n = undefined;
  // all OK
  expect(n).not.toBeNull();
  expect(n).not.toBeDefined();
  expect(n).toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  // all OK
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

数値

test('two plus two', () => {
  const value = 2 + 2;
  // all OK
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // 数値の場合は、toBe と toEqual は同等である
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

浮動小数点の値が同一であるか確認する場合はtoBeCloseToを使う。
→丸め込み誤差が原因で期待通り動作しないため

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  expect(value).toBe(0.3);        // NG
  expect(value).toBeCloseTo(0.3); // OK
});

toBeでもOKになる場合はあるがtoBeCloseToと使い分けるのは面倒なので常に後者を使う方が良さそう。

test('adding floating point numbers', () => {
  const value = 0.5 + 0.25;
  expect(value).toBe(0.75);        // OK
  expect(value).toBeCloseTo(0.75); // OK
});

文字列

正規表現を使用できる。

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/); // OK
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/); // OK
});

配列と反復可能なオブジェクト

toContainを使用すると、配列や反復可能なオブジェクトに特定のアイテムが含まれていることを確認できる。

const shoppingList = [
  'apple',
  'orange',
  'banana',
];

test('toContain check', () => {
  expect(shoppingList).toContain('banana'); // OK
  expect(new Set(shoppingList)).toContain('apple'); // OK
});

例外

toThrowを使用すると、例外をスローすることを確認できる。

function doAction() {
  throw new Error('test error throw function.');
}

test('throw Error check', () => {
  expect(doAction).toThrow(); // OK
  expect(doAction).toThrow(Error); // OK

  // エラーメッセージまたは正規表現指定も可能
  expect(doAction).toThrow('test error'); // OK
  expect(doAction).toThrow('error test'); // NG
  expect(doAction).toThrow('test error throw function.'); // OK
  expect(doAction).toThrow(/throw/); // OK
});

その他

公式リファレンスを参照のこと(matcherだけでもかなりの数がある)
https://jestjs.io/docs/ja/expect

Testing Asynchronous Code

テスト用の関数作成

// 2000ms後に引数callbackを呼ぶだけの関数
const testCallback = (callback) => {
  setTimeout(() => callback('success'), 2000);
};

// promiseサンプル
// 引数がtrueならばresolve(), falseならばreject()する
const testPromise = isSuccess => {
  return new Promise((resolve, reject) => {
    isSuccess ? resolve('success') : reject('error');
  });
};

module.exports = {
  testCallback: testCallback,
  testPromise: testPromise
}

コールバック

fetchDataが完了した時点でテスト終了となりcallbackが呼ばれないため、意図した動作にならない。

test('Async callback check', () => {
  function callback(data) {
      expect(data).toBe('success');
  }

  fetchData(callback);
});

doneという1つの引数を利用する。
Jestはdoneコールバックが呼ばれるまで待ち、呼ばれたらテスト終了となる。
これで意図したように動作する。

const fetchData = require('../async');

test('Async callback check', done => {
  function callback(data) {
    try {
      expect(data).toBe('success'); // OK
      done();
    } catch (error) {
      done(error);
    }
  }

  fetchData(callback);
});

Promises

テストからpromiseを返すと、Jestはそのpromiseがresolve()されるまで待機する。
promiseがreject()された場合は、テストは自動的に失敗する。

test('Async promise(resolve) check', () => {
  return myAsync.testPromise().then(data => {
    expect(data).toBe('success'); // OK
  });
});

promiseがreject()されることを期待するケースでは.catchメソッドを使用する。
想定した数のアサーションが呼ばれたことを確認するためexpect.assertionsを必ず追加すること。

test('Async promise(reject) check', () => {
  expect.assertions(1);
  return myAsync.testPromise(false).catch(err => {
    expect(err).toMatch('error'); // OK
  });
});

.resolves / rejects

expect文で.resolvesマッチャを使用することもできる。

test('Async promise(resolve) check', () => {
  return expect(myAsync.testPromise(true)).resolves.toBe('success'); // OK
});

同様に.resolvesマッチャを使用することもできる。
promiseが成功した場合は、テストは自動的に失敗します。

test('Async promise(reject) check', () => {
  return expect(myAsync.testPromise(false)).rejects.toMatch('error'); // OK
});

Async / Await

asyncawaitをテストで使用できる。
testに渡す前の関数にasyncキーワードを記述すれば良い。

// resolve.
test('Async promise(async/await) check', async () => {
  const data = await myAsync.testPromise(true);
  expect(data).toBe('success'); // OK
});

// reject.
test('Async promise(async/await) check', async () => {
  expect.assertions(1);
  try {
    await myAsync.testPromise(false);
  } catch (e) {
    expect(e).toMatch('error'); // OK
  }
});

asyncawait.resolvesまたは.rejectと組み合わせることもできる。

test('Async promise(async/await) check', async () => {
  await expect(myAsync.testPromise(true)).resolves.toBe('success'); // OK
});

test('Async promise(async/await) check', async () => {
  await expect(myAsync.testPromise(false)).rejects.toMatch('error'); // OK
});

Setup and Teardown

テストごとにセットアップ作業を繰り返し実行する

多くのテストで繰り返し行う必要がある処理がある場合は、beforeEachafterEachを使用します。

// テスト開始前に実行したい処理
beforeEach(() => {
  console.log('call beforeEach');
});

// テスト終了後に実行したい処理
afterEach(() => {
  console.log('call afterEach');
});

test('dummy test 1', () => {
  expect(true).toBeTruthy();
});

test('dummy test 2', () => {
  expect(true).toBeTruthy();
});

このテストを実行すると、以下の順に処理が実行される。

  1. beforeEach
  2. dummy test 1
  3. afterEach
  4. beforeEach
  5. dummy test 2
  6. afterEach

ワンタイムセットアップ

セットアップがファイルの先頭で1回だけ実行される。

beforeAll(() => {
  console.log('call beforeAll');
})

afterAll(() => {
  console.log('call afterAll');
})

test('dummy test 1', () => {
  expect(true).toBeTruthy();
});

test('dummy test 2', () => {
  expect(true).toBeTruthy();
});

このテストを実行すると、以下の順に処理が実行される。

  1. beforeAll
  2. dummy test 1
  3. dummy test 2
  4. afterAll

スコープ

describeブロックを使って複数のテストをグループ化することができる。
beforeafterブロックはdescribeブロックの中のテストにだけ適用される。

// ファイル内のすべてのテストに作用する
beforeEach(() => {
  console.log('beforeEach');
});

test('dummy test 1', () => {
  console.log('test 1');
  expect(true).toBeTruthy();
});

test('dummy test 2', () => {
  console.log('test 2');
  expect(true).toBeTruthy();
});

describe('dummy describe', () => {
  // describeブロック内のテストにのみ作業する
  beforeEach(() => {
    console.log('beforeEach in describe');
  });

  test('dummy test 1 in describe', () => {
    console.log('test 1 in desc');
    expect(true).toBeTruthy();
  });

  test('dummy test 2 in describe', () => {
    console.log('test 2 in desc');
    expect(true).toBeTruthy();
  });
});

このテストを実行すると、以下の順に処理が実行される。
"beforeEach"は常に呼び出され、"beforeEach in describe"describeブロック内でのみ呼び出されていることがわかる。

  1. beforeEach
  2. test 1
  3. beforeEach
  4. test 2
  5. beforeEach
  6. beforeEach in describe
  7. test 1 in desc
  8. beforeEach
  9. beforeEach in describe
  10. test 2 in desc

describeおよびtestブロックの実行順序

公式console.log()を埋め込んだ具体的コードあり

Mock Functions

モック関数を利用する

確認用のモジュールを作成する。

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

module.exports = {
  forEach: forEach
}

モック関数を利用する。全てのモック関数は.mockプロパティがあり、モック関数呼び出し時のデータと、関数の返り値が記録されている。

test('モック関数を利用する', () => {
  const myMock = require('../mock');

  const mockCallback = jest.fn(x => 42 + x);
  myMock.forEach([0, 1], mockCallback);

  // mock関数は2回呼ばれる
  expect(mockCallback.mock.calls.length).toBe(2);
  // 1回目の呼び出し時の引数は0
  expect(mockCallback.mock.calls[0][0]).toBe(0);
  // 2回目の呼び出し時の引数は1
  expect(mockCallback.mock.calls[1][0]).toBe(1);
  // 1回目の呼び出し時の戻り値は42
  expect(mockCallback.mock.results[0].value).toBe(42);
  // 2回インスタンス化された
  expect(mockCallback.mock.instances.length).toBe(2);
});

モックの戻り値

モック関数は、テスト中のコードにテスト用の値を注入するのにも利用できる。

test('モックの戻り値', () => {
  const myMock = jest.fn();
  console.log(myMock()); // > undefined
  myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
  console.log(myMock(), myMock(), myMock(), myMock());
});
test('モックの戻り値', () => {
  const filterTestFn = jest.fn();

  // 1回目のコール時はtrueを返し、2回目のコール時はfalseを返す
  filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

  const result = [11, 12].filter(num => filterTestFn(num));
  console.log(result); // [11] (trueのみが抽出されている)
  console.log(filterTestFn.mock.calls); // [ [ 11 ], [ 12 ] ]
});

モックの実装

jest.fnまたはモック関数のmockImplementationOnceメソッドを利用することで実現できます。

test('モックの実装', () => {
  const myMockFn = jest.fn(cb => cb(null, true));

  myMockFn((err, val) => console.log(val));
});

mockImplementationメソッドは他のモジュールによって作成されたモック関数のデフォルトの実装を定義したいときに使用すると便利です。

// sum.js
function sum(a, b) {
    return a + b;
}
module.exports = sum;

// test
test('モックの実装_mockImplementationOnceメソッド', () => {
  jest.mock('../sum')
  const sum = require('../sum');

  sum.mockImplementation((a, b) => a * b);
  sum(2, 3); // 6
});

関数への複数回への呼び出しで異なる結果を得るようモック関数を再作成する必要がある場合

test('モックの実装_複数回の呼び出しで異なる結果を得る', () => {
  const myMockFn = jest
    .fn()
    .mockImplementationOnce(cb => cb(null, true))
    .mockImplementationOnce(cb => cb(null, false));

  myMockFn((err, val) => console.log(val)); // true
  myMockFn((err, val) => console.log(val)); // false
});

mockImplementationOnceで定義された実装を全て使い切った場合はjest.fnのデフォルトの実装を実行する。

test('モックの実装_mockImplementationOnceメソッドを使い切った場合', () => {
  const myMockFn = jest
    .fn(() => 'default')
    .mockImplementationOnce(() => '1st call')
    .mockImplementationOnce(() => '2nd call');

  console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
});

カスタムマッチャ

マッチャのカスタムができる。

// ======== カスタムマッチャ ========
test('カスタムマッチャ', () => {
  const myMock = jest
    .fn(scalar => 42 + scalar)
    .mockName('add42');

  myMock(1);
  myMock(2);
  myMock(3);
 
  // 1度は呼ばれた
  expect(myMock).toHaveBeenCalled();
  expect(myMock.mock.calls.length).toBeGreaterThan(0);

  // 1度は指定の引数で呼ばれた
  expect(myMock).toHaveBeenCalledWith(1);
  expect(myMock.mock.calls).toContainEqual([1]);
  expect(myMock.mock.calls).toContainEqual([2]);
  expect(myMock.mock.calls).toContainEqual([3]);
  // expect(myMock.mock.calls).toContainEqual([4]); // NG

  // 最後に指定の引数で呼ばれた
  expect(myMock).toHaveBeenLastCalledWith(3);
  expect(myMock.mock.calls[myMock.mock.calls.length - 1]).toEqual([
    3
  ]);
});

関数のモックについて

spyOnを使用する。
使い方:jset.spyOn(モジュール変数, '関数名')

// spyOn確認用
const item = {
  title() {
    return 'item title';
  }
};

module.exports = {
  item: item
}
describe('mock spyOn test', () => {
  const { item } = require('../mock');

  test('item title test', () => {
    // mockReturnValueOnceによって自由にモック化できる。
    // jest.spyOnだけでは(実際の)モック化されていない関数が実行される。
    // jset.spyOn(モジュール変数, '関数名')
    const spy = jest
      .spyOn(item, 'title')
      .mockReturnValueOnce('item sub title');

    // mockReturnValueOnceによって'item title'ではなくなる。
    expect(item.title()).toBe('item sub title'); // OK
    expect(spy).toHaveBeenCalled();

    spy.mockRestore();
  });
});