びぼーろくっ!

誰かに見せるわけでもないけど、備忘録として。。

モジュール内で呼び出されてるメソッドをスタブ化できるproxyQuireが便利すぎた。

テストを書いて困ったことがあったので、備忘録して書いておきます。
実装側はこんな感じ。

//Goods.js テスト対象
import {readFileSync} from 'fs';

const goodThings = JSON.parse(readFileSync('/rankValues/good'));
const sosoThings = JSON.parse(readFileSync('/rankValues/soso'));
const badThings = JSON.parse(readFileSync('/rankValues/bad'));

export default class Goods {
  get (status) {
    let goods;
    const goodsList = [goodThings, sosoThings, badThings];
    goodsList.forEach((key) => {
      if (status === key) goods = goodsList[key];
    });
    return goods;
  }
}

こんな感じのモジュールがあって、
無心でテストを書いてたらイケてないことに気付きました。
(途中で気付いたのですが、最後まで無心ならこうなってただろうという想定で書きました)

//BadTest.js イケてなかったテスト
import 'mocha';
import assert from 'power-assert';

import {readFileSync} from 'fs';

import Goods from '../../src/Goods';

const goodThings = JSON.parse(readFileSync('/rankValues/good'));
const sosoThings = JSON.parse(readFileSync('/rankValues/soso'));
const badThings = JSON.parse(readFileSync('/rankValues/bad'));

describe('テスト.だめな例', () => {
  let goods = new Goods();
  });
  it('Good!', () => {
    const result = goods.get('good');
    assert.deepEqual(result, goodThings);
  });
  it('Soso!', () => {
    const result = goods.get('soso');
    assert.deepEqual(result, sosoThings);
  });
  it('Bad....', () => {
    const result = goods.get('bad');
    assert.deepEqual(result, badThings);
  });
});

このテストケースの問題点としては、実装されたデータを使用しているという点ですね。
テストケースのデータは定数(enumとか?)以外は
テスト用のデータを使うのがいいですよね。。

例えば
あくまでテストのデータの中身を検証するテストではなくて、get()の実装を見るテストなので・・・

ただし変更が困難なのがconstで定義されたgoodsがclassの外側に出ていること。
これはimportで読み込まれた時点で処理がスタート(この場合はfs.readFileSync()とJSON.parse())してるため
うかつに変えることができません・・・

また、今回のソースとは異なりますが

//hoge.jsのimport文
import Foo from 'foo';
// foo.jsのimport文
import Bar from 'bar';

のように複雑にimportされているテストだと、
hoge.jsを成功させるにはFooとBarの処理を想定してテストを書かなきゃいけません。
これじゃ単体テストの意味がありません・・・。

そこで必要になるのがproxyQuireです。

おふぃ
GitHub - thlorenz/proxyquire: 🔮 Proxies nodejs require in order to allow overriding dependencies during testing.

簡単にいえばimportなどで呼ばれたモジュールやexport以外のモジュールや
関数などをスタブ化出来るというものです。

sinon.jsと組み合わせで実装していきます。

//Test.js
import 'mocha';
import assert from 'power-assert';
import proxyQuire from 'proxyquire';
import sinon from 'sinon';

import Goods from '../../src/Goods';;

// この3行はテストデータです。実装側のファイルは使用しません(読まない)。
const goodThings = {good: {goods: ['tv','tablet','guitar'],}};
const sosoThings = {soso: {goods: ['fruit','meet','juice'],}};
const badThings = {bad: {goods: ['shit','paper','trash'],}};

function createProxyQuire() {
  // スタブ化していきます。今回スタブ化するのはfs.readFileSyncのみなので
  // 3種類の引数が渡されたらテストデータを返すようスタブを作成します。
  const stubReadFileSync = sinon.stub();
    stubReadFileSync.withArgs('/rankValues/good')
    .returns(JSON.stringify(goodThings));

    stubReadFileSync.withArgs('/rankValues/soso')
    .returns(JSON.stringify(sosoThings));

    stubReadFileSync.withArgs('/rankValues/bad')
    .returns(JSON.stringify(badThings));

  // ProxyQuireを使用します。
  // 第一引数にテスト対象のモジュール(相対パス),
  // 第二引数にスタブ化したいものをkey-value形式でかいていきます。
  const proxyQuireAuthService = proxyQuire('../../src/Goods',{
    fs:{
      readFileSync: stubReadFileSync
    }
  });
  return proxyQuireAuthService;
}

describe('proxyQuireを使ったテスト', () => {
  const Goods = createProxyQuire();
  });
  it('Good!', () => {
    const result = Goods.default.prototype.get('good');
    assert.deepEqual(result, goodThings);
  });
  it('Soso!', () => {
    const result = Goods.default.prototype.get('soso');
    assert.deepEqual(result, sosoThings);
  });
  it('Bad....', () => {
    const result = Goods.default.prototype.get('bad');
    assert.deepEqual(result, badThings);
  });
});

本日はこんな感じです。