Jest のモック関数を整理する
npq
という、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxios
をnode-fetch
で代替するというissueがあったのでやってみた。
置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
jest.mock や mockImplementation や jest.spyOnに対する理解が曖昧だったので、整理したいと思う。
コードはこのレポジトリにまとめた。
github.com
まずは、単純な関数のモックから整理していきたい。
下のように、第2引数とランダムな値を足した値を返す関数があるとする。
function random(a, b, c) { return b + Math.random(); } module.exports = random;
この関数のテストでは、便宜上、ランダムな値は無視したい。
なぜなら、無視しないと返り値が予測できず、テストが通らないからだ。
const random = require("./random"); it("random function", () => { expect(random(1, 2, 3)).toBe(2); // Expected: 2 // Received: 2.119340184508286 });
モックする1つの方法として、jest.fn を用い、random
に新しい、未使用の mock functionを代入してしまう方法がある。
しかし、この方法では、後続のテストが壊れてしまう恐れがある。
let random = require("./random"); random = jest.fn(() => 2) it("random function", () => { expect(random(1, 2, 3)).toBe(2); // pass expect(random(1, 3, 5)).toBe(3); // Expected: 3 // Received: 2 });
そこで、random
モジュールをモックし、random
関数が第2引数を返すようにモックすれば、うまくいく。
const random = require("./random"); jest.mock('./random') random.mockImplementation((a, b, c) => b); it("random function", () => { expect(random(1, 2, 3)).toBe(2); // pass expect(random(1, 3, 5)).toBe(3); // pass });
これをnode-fetch
に対するモックでも応用してみる。
下のように、Promise
を返す関数があるとする。
const fetch = require("node-fetch"); function getByFetch() { const data = fetch("http://example.com/"); return data; } module.exports = getByFetch;
これに対するテストは、node-fetch
モジュールをモックし、fetch
関数がfoo
を返すようにモックすれば、うまくいく。
const fetch = require("node-fetch"); const getByFetch = require("./fetch"); jest.mock("node-fetch"); fetch.mockImplementation(() => { return "foo"; }); it("fetch", () => { const data = getByFetch(); expect(data).toBe("foo"); // pass });
次に、オブジェクト内の関数のモックについて整理していく。
このようなオブジェクトがあるとする。
const obj = { random: function () { return Math.random(); }, randomPlusOne: function () { return Math.random() + 1; }, foo: function () { return "foo"; }, }; module.exports = obj;
これに対するテストでモックする1つの方法としては、obj
モジュールごとモックしてしまう方法がある。
const obj = require("./object"); jest.mock("./object", () => { return { random: () => 1, randomPlusOne: () => 2, foo: () => 'foo', }; }); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
これは、次のようにも書ける。
const obj = require("./object"); jest.mock("./object"); obj.random.mockImplementation(() => 1); obj.randomPlusOne.mockImplementation(() => 2); obj.foo.mockImplementation(() => 'foo'); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
しかしこれらは、obj
モジュールごとモックしているため、例えばfoo
をモックし忘れたら、存在していないことになってしまう。
const obj = require("./object"); jest.mock("./object"); obj.random.mockImplementation(() => 1); obj.randomPlusOne.mockImplementation(() => 2); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // Expected: "foo" // Received: undefined });
jest.spyOnを用いれば、その心配はなくなる。
const obj = require("./object"); jest.spyOn(obj, 'random').mockImplementation(() => 1); jest.spyOn(obj, 'randomPlusOne').mockImplementation(() => 2); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
これをaxios
に対するモックでも応用してみる。
下のように、Promise
を返す関数があるとする。
const axios = require("axios"); function getByAxios() { const data = axios.get("http://example.com/"); return data; } module.exports = getByAxios;
これに対するテストは、以下のように書けばうまくいく。
const axios = require("axios"); const getByAxios = require("./axios"); jest.spyOn(axios, 'get').mockImplementation(() => Promise.resolve('foo')); it("axios", async () => { const data = await getByAxios(); expect(data).toBe("foo"); });
理解が少し難しいjest.mock
/mockImplementation
/jest.spyOn
あたりを整理した。