npq
という、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxios
をnode-fetch
で代替するというissueがあったのでやってみた。
github.com
github.com
置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
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);
});
モックする1つの方法として、jest.fn を用い、random
に新しい、未使用の mock functionを代入してしまう方法がある。
しかし、この方法では、後続のテストが壊れてしまう恐れがある。
let random = require("./random");
random = jest.fn(() => 2)
it("random function", () => {
expect(random(1, 2, 3)).toBe(2);
expect(random(1, 3, 5)).toBe(3);
});
そこで、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);
expect(random(1, 3, 5)).toBe(3);
});
これを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");
});
次に、オブジェクト内の関数のモックについて整理していく。
このようなオブジェクトがあるとする。
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);
expect(obj.randomPlusOne()).toBe(2);
expect(obj.foo()).toBe('foo')
});
これは、次のようにも書ける。
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);
expect(obj.randomPlusOne()).toBe(2);
expect(obj.foo()).toBe('foo')
});
しかしこれらは、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);
expect(obj.randomPlusOne()).toBe(2);
expect(obj.foo()).toBe('foo')
});
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);
expect(obj.randomPlusOne()).toBe(2);
expect(obj.foo()).toBe('foo')
});
これを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
あたりを整理した。