なぜJestが全てをモックできるのか

[Jest 0.2.2](https://github.com/facebook/jest/tree/4970dab29056fe1718dcce9b5f2cefc73e51e6c7)の時点の内容であり、後のバージョンだと変更されている可能性がある。

## TL;DR

[Contexify](https://github.com/brianmcd/contextify)が作り出したsandbox内でモックを返すように`require`を置き換え、テストコードを実行している。

## 拾い読みTestRunner

CLIで入力されたパスを元にテストを実行する最もシンプルな形であろう`TestRunner.prototype.runTest`から拾っていく。

```js:src/TestRunner.js#320
var configDeps = this._loadConfigDependencies();

var env = new configDeps.testEnvironment(config);
var testRunner = configDeps.testRunner;
```

まずは設定に依存する環境を構築する。設定のデフォルトの値は別の場所に記述されている。

```js:src/lib/utils.js#15
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](https://github.com/tmpvar/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だ。

```js
var window = require('jsdom').jsdom().parentWindow;
window.run('x = 1;');  // => 1
console.log(window.x);  // => 1
```

Contextifyによりこのようなことが可能になる。つまりここでは、Jasmineを`window`内部で実行していると言える。そうしてロードされたJasmineに対して、テストを実行させる。

```js:src/jasmineTestRunner/jasmineTestRunner.js#238
// 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`を見る。

```js:src/HasteModuleLoader/HasteModuleLoader.js#211
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などをつけて返している。

モジュールの内容に補完された変数群を与え、実行する。

```js:src/HasteModuleLoader/HasteModuleLoader.js#245
utils.runContentWithLocalBindings(
  this._environment.runSourceText.bind(this._environment),
  moduleContent,
  modulePath,
  moduleLocalBindings
);
```

それでは、実際のモジュール実行部分を見てみよう。`contextRunner`が`JSDomEnvironment.prototype.runSourceText`だ。

```js:src/lib/utils.js#341
var wrapperFunc = contextRunner(
  '(function(' + boundIdents.join(',') + '){' +
  scriptContent +
  '\n})',
  scriptPath
);
```

```js:src/lib/utils.js#357
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`内部だからだ。