console.lealog();

@leader22のWeb系に関する勉強めもブログですのだ

テストでモックできなくて困るNode.jsのモジュールの書き方

こういう書き方にしてしまったせいで、テストの時にうまくモックできず困ったよというメモ。

問題のコード

こういうやつ。

function foo(id) {
  bar(id);

  // ...
}

function bar(id) {
  // some heavy logics...
}

module.exports = {
  foo,
  bar,
};

モジュール内が関数だけで構成されてるのは○。
ただし、テスト的には都合が悪い。

何に困るか

テストでこの関数をモックしたい・・って時に困る。

さっきのコード例で、`foo()`をテストするときに、`bar()`をモックしたいとする。

jestだとこんな風にするはず。

const mod = require('../mod');

describe('foo test', () => {
  let barSpy;
  beforeEach(() => {
    barSpy = jest
      .spyOn(mod, 'bar')
      .mockImplementation(jest.fn);
  });
  afterEach(() => {
    barSpy.mockRestore();
  });

  test('should call bar', () => {
    mod.foo();
    expect(barSpy).toHaveBeenCalledTimes(1);
  });
});

一見なんの問題もなさそうですが、実際はモックしたはずの`bar()`は呼ばれません。

なぜか

`mod.bar`は確かにモックされてるが、肝心の`mod.foo`内で呼ばれる`bar()`は、モックされてない`bar()`だから。

`foo()`のスコープで見ると、関数`bar`はあくまでファイル内にある`bar`であり、外でモックされてるかどうかなんか知ったこっちゃあない状態になってる。

こうすればよい

const mod = (module.exports = {});

mod.foo = function foo(id) {
  mod.bar(id);

  // ...
}

mod.bar = function bar(id) {
  // some heavy logics...
}

うーん、初歩的なミス・・。

ちなみに

エクスポートする側でちゃんと親の参照を保っていても、インポートする側 = 使う側がこうしてたらダメ。

const { foo } = require('./mod');

const usecase = (module.exports = {});

usecase.baz = function() {
  foo();
};

この場合に、`foo`がモックできなくて困る。

const mod = require('./mod');

const usecase = (module.exports = {});

usecase.baz = function() {
  mod.foo();
};

こうなってないとダメ。