Jest 0.2.2の時点の内容であり、後のバージョンだと変更されている可能性がある。
TL;DR
Contexifyが作り出したsandbox内でモックを返すようにrequire
を置き換え、テストコードを実行している。
拾い読みTestRunner
CLIで入力されたパスを元にテストを実行する最もシンプルな形であろうTestRunner.prototype.runTest
から拾っていく。
var configDeps = this._loadConfigDependencies();
var env = new configDeps.testEnvironment(config);
var testRunner = configDeps.testRunner;
まずは設定に依存する環境を構築する。設定のデフォルトの値は別の場所に記述されている。
var DEFAULT_CONFIG_VALUES = {
cacheDirectory: path.resolve(__dirname, '..', '..', '.haste_cache'),
globals: {},
moduleLoader: require.resolve('../HasteModuleLoader/HasteModuleLoader'),
modulePathIgnorePatterns: [],
testDirectoryName: '__tests__',
testEnvironment: require.resolve('../JSDomEnvironment'),
testFileExtensions: ['js'],
moduleFileExtensions: ['js', 'json'],
testPathDirs: ['<rootDir>'],
testPathIgnorePatterns: ['/node_modules/'],
testRunner: require.resolve('../jasmineTestRunner/jasmineTestRunner'),
};
testEnvironment
として指定されているJSDomEnvironment
のコンストラクタ内を見ると、jsdom().parentWindow
というjsdomを見たことある人間には見覚えのある記述があるので、ざっくりとtestEnvironment
はjsdomのwindowを保持していると考えられる。
この後しばらくTestRunner.prototype.runTest
は設定に依存する記述があるので読み飛ばしてゆくと、testRunner(config, env, moduleLoader, testFilePath)
とjasmineTestRunnerを実行する。
jasmineTestRunnerは冒頭でenvironment.runSourceText(jasmineFileContent, JASMINE_PATH)
を実行しており、JSDomEnvironment.prototype.runSourceText
の実装を辿ると、JSDomEnvironment
内部に保持しているwindow
オブジェクトのrun
へと移譲されている。
jsdomのwindow.run
はwindow
をグローバルコンテキストとして、与えたJavaScriptコードをwindow
より外部に影響を与えず実行する。これを可能にしているのがContextifyだ。
var window = require('jsdom').jsdom().parentWindow;
window.run('x = 1;'); // => 1
console.log(window.x); // => 1
Contextifyによりこのようなことが可能になる。つまりここでは、Jasmineをwindow
内部で実行していると言える。そうしてロードされたJasmineに対して、テストを実行させる。
// Run the test by require()ing it
moduleLoader.requireModule(testPath, './' + path.basename(testPath));
jasmine.getEnv().execute();
テストコードを読み込む際はmoduleLoader.requireModule
という、直接window.run
を呼ぶ方法ではない方法をとっている。そこで、moduleLoader
の内部に注目してみよう。moduleLoader
は最初のデフォルト設定どおりの場合、HasteModuleLoaderにて実装されている。
拾い読みHasteModuleLoader
Loader.prototype.requireModule
はコメントにもあるとおり、モックされていないモジュールを返すためのものである。だが、前述の通りモジュールを読み込むのはwindow
というグローバルな、なおかつrequire
が存在しない環境である。
いかにrequire
を実現しているかはLoader.prototype.requireModule
の最後で呼び出されているLoader.prototype._execModule
を見る。
moduleObj.require = this.constructBoundRequire(modulePath);
var moduleLocalBindings = {
'module': moduleObj,
'exports': moduleObj.exports,
'require': moduleObj.require,
'__dirname': path.dirname(modulePath),
'__filename': modulePath,
'global': this._environment.global,
'jest': this._builtInModules['jest-runtime'](modulePath).exports
};
ここでモジュールをwindow
で実行した際に補完されるであろうmodule
やexports
, require
が定義されている。Loader.prototype.constructBoundRequire
の内部では、Loader.prototype.requireModuleOrMock
という読んで字の如しなfunctionにrequire.resolve
相当のfunctionなどをつけて返している。
モジュールの内容に補完された変数群を与え、実行する。
utils.runContentWithLocalBindings(
this._environment.runSourceText.bind(this._environment),
moduleContent,
modulePath,
moduleLocalBindings
);
それでは、実際のモジュール実行部分を見てみよう。contextRunner
がJSDomEnvironment.prototype.runSourceText
だ。
var wrapperFunc = contextRunner(
'(function(' + boundIdents.join(',') + '){' +
scriptContent +
'\n})',
scriptPath
);
wrapperFunc.apply(null, bindingValues);
boundIdents
, bindingValues
は先ほど定義されたモジュールロード時に補完される変数群から導き出されたものである。ざっくり言えば、window
上でmodule
, require
などNode環境に欠けているものを変数として取り、モジュールコードをそのまま実行するfunctionを一時的に定義してNode環境へと返し、Node環境側で変数群を与えて実行しているということだ。
そしてrequire
はLoader.prototype.requireModuleOrMock
なので、通常であればモックを返すことになり、jest.dontMock
されていれば同じ方法でロードされる。
これがJestの「全てのrequire
がモックを返す」という設計の根幹だ。
つまり
テストコードを動かしている環境はjsdomのwindow
であり、Contextifyが作り出したsandboxの内部である。
ロードする際に実行されるfunctionでjest
という引数が定義されているから、テストコード内でjest.dontMock
という変数を疑似グローバルに参照できる。
require('react/addons')
すればRenderIntoDocument
が普通に使えるのも、テストコードを実行する環境がjsdomのwindow
内部だからだ。