npm rewire を試す

2018年7月18日 0 開発 ,

最近 React SFC (Stateless Functional Components) や Redux の影響を受けて、ES.next な環境でもクラスより関数で実装するようにしている。ES Modules であれ Node であれ個別に関数を外部公開できるからクラスを使用せずとも実装には十分だし、むしろ関数を積極的に採用することで外部依存を引数に限定できる。

しかし困ったことが。非公開の関数を単体テストする手段がない。

そもそも非公開なものをテスト対象とするのは悪手では?と言われればそのとおり。しかし公開関数が単一単純でも内部で多くの非公開な関数へ依存しているなら、それらを個別に単体テストしたくなるだろう。外部からみたら公開関数を単体テストしているつもりでも実際には結合テストなわけで。

クラスなら今のところアクセス指定子がないため、非公開メソッドは命名を工夫する慣習 (アンダースコアを接頭辞にする) があるだけだから

export default class Sample {
  publicMethod () {
  }

  _privateMethod () {
  }
}

と定義してあれば _privateMethod を呼び出せる。

一方、モジュール スコープに定義された非公開の関数を外部参照するためには Java や C# でいうところのリフレクション的な処理が必要になる。というわけで、ようやく rewire の話。

rewire

非公開な関数をテストする方法やツールについて調べたたら以下の記事をみつけた。

まさにほしかったもの。記事が書かれたのは 2013 年ということもあり ES2015 以降で動くのか不安だったが

を見ると現在もメンテされているようだ。では試してみよう。まずは環境から。package.json からテストに関するものを抜粋。

{
  "scripts": {
    "test": "mocha --timeout 50000 --require babel-register src/**/*.test.js"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-power-assert": "^2.0.0",
    "babel-register": "^6.26.0",
    "mocha": "^5.2.0",
    "power-assert": "^1.6.0",
    "rewire": "^4.0.1"
  }
}

この環境で shortcode.js というファイルに定義された以下の非公開 API をテストしてみる。

const trimLineBreak = (text) => {
  return text.replace(/^[\n]|[\n]$/g, '')
}

テスト コードは以下。

import assert from 'assert'
import Rewire from 'rewire'

describe('trimLineBreak', () => {
  const Module = Rewire('./shortcode.js')
  const trimLineBreak = Module.__get__('trimLineBreak')

  it('trimLineBreak', () => {
    let actual = trimLineBreak('\nText\n')
    assert(actual === 'Text')

    actual = trimLineBreak('\n\nText\n')
    assert(actual === '\nText')
  })
})

npm test を実行すると非公開にも関わらず trimLineBreak を呼び出しテストは成功。assert の引数を書き換えて意図的にテストを失敗させても、きちんと power-assert による失敗の詳細が表示された。

rewire の使い方について。まずは rewire を読み込む。

import Rewire from 'rewire'

次に非公開の関数が定義されているモジュールを rewire 経由で読み込む。

const Module = Rewire('./shortcode.js')

最後にモジュールから非公開 API の名前を指定して参照を得る。

const trimLineBreak = Module.__get__('trimLineBreak')

これで非公開 API を呼び出せる。なお関数だけでなく変数も読み込めるため、テストに非公開な定数が必要な場合でも rewire でいける。実に便利。

余談だがクラスの非公開メソッドについて。tc39/proposals によると 2018/7 時点で

が Stage 3 なので近いうちに事情が変わる可能性はある。これに対して rewire が効くのかは babel-preset-env にきたら改めて試すかも。