🧊

Jestを使ってみてのハマりどころメモ

JestいいですよねJest。
あれこれプラグインとかライブラリとか入れなくてもだいたいのことができて。

さて、この1ヶ月くらいひたすらJestでテストを書き続けて、ハマったとこをメモ。
逆に言えば、ここに書いてないことでは一切困ってなくて、Jest最高って感じ。

Karmaみたいなブラウザでテストする機能さえつけばもう一生ついていきます感すらある。

before / afterでもasync

Docsにそれらしいコードはないけどできる。

beforeAll(done => {
  // some async tasks
  done();
});
beforeEach(async () => {
  await asyncTask();
});
afterEach(async done => {
  await asyncTask();
  asyncTask2(done);
});
afterAll(() => {
  // some sync tasks
});

Mochaとかだと`before` / `after`やけど、Jestは`beforeAll` / `afterAll`である。

Matcherあれこれ

`toHaveProperty`のキー

const obj = {
  arr: [
    { x: 1 },
    { x: 2 },
  ],
  'hyp-hen': 'Hello'
};

expect(obj).toHaveProperty('arr.1.x', 2);
expect(obj).toHaveProperty('hyp-hen', 'Hello');

ってな感じで数値なプロパティもそのまま書けばいい。
ハイフンも気にせず書ける。

`toHaveBeenCalledWith`

const spy = jest.fn();
spy(1);
spy(3);
spy(5);

expect(spy).toHaveBeenCalledWith(3);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveBeenCalledWith(1);

続けて呼べば次の`mock.calls`を見てくれる・・のではなく、そのモックがテストを通してその引数で呼ばれたかどうかがわかる(順不同)。
どれかひとつでも呼ばれてれば、パスする。

`mockReturnValue`と`mockImplementation`

const foo1 = jest.fn().mockReturnValue('foo');
const foo2 = jest.fn().mockImplementation(() => 'foo');

この2つは同じではないことに気づかずドハマりしてた。
`mockReturnValue()`のほうが短く書けていいやん、と。

このコードに限っていえば同じ結果になるけど、用途としては間違ってることを知るのが大事。
例えばこんな場合にハマる。

const err1 = jest.fn().mockReturnValue(Promise.reject(new Error('err')));
const err2 = jest.fn().mockImplementation(() => Promise.reject(new Error('err')));

前者は、関数を呼ぶまでもなくエラーなので、テストがコケて「なんで???」ってなります。

その名の通り、

  • `mockReturnValue()`は値を返すだけでいいとき
  • `mockImplementation()`は関数を返したいとき
    • 引数をさわりたいとき

に使いましょう。

`spyOn`するだけではダメ

Note: By default, jest.spyOn also calls the spied method. This is different behavior from most other test libraries.

って、 The Jest Object · Jest に思いっきり書いてある。

const obj = {
  foo() { console.log('foo!'); },
};

// spyOnしただけだと
jest.spyOn(obj, 'foo');
// 呼ばれちゃう
obj.foo(); // foo!

// 呼びたくないなら実装を用意する
jest.spyOn(obj, 'foo').mockImplementation();
obj.foo(); // 何も起きない

`useFakeTimers`はDateをモックしない

Jest useFakeTimers should also handle Date · Issue #2684 · facebook/jest · GitHub

なので、`setTimeout`とかには使えるけど、`Date`で時間を見て・・みたいな処理には使えない。
現時点でJestとしては対応しないらしいので、どうにかしてモックする。

おすすめは https://github.com/sinonjs/lolex です。というか、これしか選択肢なさそう。

`proxyquire`的なことがしたい

// api.js
const request = require('request-promise-native');

module.exports = {
  getFoo() {
    return request.get('/foo');
  }
};

こういうモジュールをテストしたい場合にどうするか。
選択肢はいくつかある。

`jest.mock`するパターン

`node_modules`なやつなので、`__mocks__`ディレクトリでモックを定義する。
そうじゃないなら、`jest.mock()`でモックする。

個人的には、影響範囲が広すぎてあまり好きではないパターンかも。

Manual Mocks · Jest

`spyOn`で凌ぐパターン

let getSpy;
beforeEach(async () => {
  const request = require('request-promise-native');
  getSpy = jest.spyOn(request, 'get');
});
afterEach(() => {
  getSpy.mockRestore();
});

test('should get foo', async () => {
  getSpy.mockImplementationOnce(() => Promise.resolve('foo'));

  const foo = await api.getFoo();
  expect(foo).toBe('foo');
  expect(getSpy).toHaveBeenCalledTimes(1);
});

このように、実際にテストが走る前に、中で使ってるモジュールを`spyOn`でモックする。
(`require()`する度に違う参照が返ってくるモジュールは無理やけどそんなんは稀なはず)

基本的に、

  • 準備段階では`mockImplementation()`
  • 各テスト段階では`mockImplementationOnce()`

って感じの使い分けにするのが好き。

autoMockはデフォルトでオフ

インターネット情報だと古い記事が引っかかることも多くて、無駄な情報で混乱する。

かつてのJestは、`require()`したモジュールが基本的に全部モックだったらしい。
なのでテスト対象のモジュールを呼ぶたびに`unmock()`しないといけなくて・・みたいな記事がいっぱい出てくる。

が、v15からはそんなこともなく、他のテストフレームワークと同じように、普段のコードと同じように何もしなくなってる。

Jest 15.0: New Defaults for Jest · Jest

jest.xxx()はファイルスコープ

`jest.mock()`とか、`jest.useFakeTimers()`とか。

そして、記述はそのファイルの先頭に巻き上げられる。
この挙動が本当にキモで、これを知らないとドハマリするはず。

// a.test.js
test('caseA-1', () => {
  jest.useFakeTimers();
});

test('caseA-2', () => {
  // use FakeTimers
});

ファイルをまたぐと関係ない。

// b.test.js
test('caseB-1', () => {
  // use RealTimers
});

CircleCIでコケる

シリアルで実行させないと都合の悪いテストがあるとコケるので、`--runInBand`か`-i`のどちからをつけて実行する。