tech::hexagram

personal note for technical issue.

ES6で書かれたUserscriptでchaiとmochaを使ったテストを走らせるための小技

github.comのcommit一覧ページで、2点のcommit hashからcompareページに簡単に遷移できる Userscript を3年ほど前に開発し、最近ES6を使ってほぼフルスクラッチで書き換えた。

その際に chaimocha を使ったユニットテストを書けるように整備した。通常のjsファイルとは異なり、気をつけないと「Userscriptは動くがテストは動かない」、「Userscriptは動かないがテストは動く」といった事態に陥る。

このエントリでは、Userscriptをtestableにするために実装上気をつけた点を紹介する。

Userscript側

テストしたい関数を1つのclassに集約する

Userscriptは、ブラウザに組み込むことで何らかの補助的動作を可能にするツールである。この「補助的動作」の根幹に当たる部分を class に切り出し、それをテストケースの対象としておくと見通しが良くなる。

冒頭に上げた github-commitdiff-viewer では、

などといったロジックを CommitDiffUIManager というclassに集約させ、このclassをテスト対象とした。

Userscriptだけで動かしたい処理とテストだけで動かしたい処理を分岐させる

Userscriptとして動かしつつ、テストも実行可能なように記述するために下記のような実装を行った。

  class TestTargetClass {
    // Userscriptの機能の根幹部分を実装
  }

  if (typeof window === 'object') {
    let testTargetClass = new TestTargetClass(...);

    testTargetClass.executeSomething();
  } else {
    module.exports = TestTargetClass;
  }

module.exports はテストだけで動かしたい部分で、ここをUserscript上で実行してしまうとエラーが発生してしまう。

image

テスト側で global.window オブジェクトをUserscriptのrequire後にmockすることで、「Userscriptをテスト側でrequireしたときにはwindowオブジェクトがない」という状況を作り出すことが出来る。

let TestTargetClass = require(path.join(__dirname, '..', 'someuserscript.user.js')); // この時点ではwindowは未定義

global.window = ...

この挙動の差を利用して、 if (typeof window === 'object')true のときはUserscriptのみで動かしたい実装を記述し、 false のときはテストのみで動かしたい実装を記述することが出来る。

テスト側

jsdomを使ってUserscriptが読み込まれるページのdocumentをmockする

let jsdom   = require('jsdom');

const dom = new JSDOM(`
// Userscriptが読み込まれるページのdocumentのHTMLのうち、
// テストに必要な部分をべた書きする
`);

global.window = dom.window;
global.$      = require('jquery');

前の項目で global.window をUserscriptのrequire後に記述するとUserscriptとテストで挙動を分けることが出来る点を紹介した。

global.window にjsdomを活用することで、Userscriptが読み込まれるページをモックでき、「DOMを探索して何かする」といった挙動の関数についてもテストが可能になる。