経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

Jest のモック関数を整理する


npqという、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxiosnode-fetchで代替するというissueがあったのでやってみた。

github.com
github.com

置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
jest.mockmockImplementationjest.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 あたりを整理した。