概要
IstanbulJS - レポート改善 - ソースコード修正 の課題の検証。
react-scripts@2.xを用いたTypeScript環境にて、IstanbulJS で生成したHTML形式のカバレッジレポートにおいて以下の問題が発見されたため、原因と解決策を調査しながら随時更新する。
🙅 関数の定義部のハイライトが戻り値の型までカバーされていない。
🙅 クラスメソッドの public キーワードがハイライトされている。
調査
JSONレポートにおけるソースマップが存在しない
TypeScript環境では、以下の流れでカバレッジレポートを生成する。
- Jest: 何らかのプログラムを呼び出す。
- ???: TypeScriptコードをJavaScriptへトランスパイルする。
-
Jest: トランスパイルされたJavaScriptコードにてテストを実行し、
IstanbulJSを呼び出す。 - IstanbulJS: JSONレポートを生成する。以降、このレポートを「一時レポート」と記載する。
- nyc: 一時レポート中のソースマップを用いてソースマッピングを行い、TypeScriptコード用のレポートを生成する。
react-scripts@2.xとreact-scripts@1.xの両環境における一時レポートを比較したところ、後者にはソースマップが含まれていないことが判った。ソースマップが無ければ nyc はソースマッピングをできないため、ハイライトが正しく行われない。
一時レポートにソースマップを含めることが解決策になりうると仮説を立てて調査を続行した。
再現
設定ファイルの作成 では、nyc が用いる一時レポートの格納先とソースマッピング後のレポートの出力先を同一にしており1、一時レポートは上書きされるため内容を確認することができない。内容を確認するため、ソースマッピング後のレポートの出力先を別のディレクトリへ変更する。
{
"reporter": [
"text-summary",
"json",
"html"
],
"report-dir": "./coverage-final",
"temp-dir": "./coverage"
}
レポートを再び生成して一時レポートを確認すると、ソースマップが含まれていないことが判る(下記画像左)。他方、react-scripts@1.xの環境では、一時レポートにソースマップが含まれている(同画像右)。
ts-jest が存在しない
react-scripts@1.xの環境でソースマップを出力しているコードを調べたところ、ts-jest の以下のコードが関わっていることが判った。
config.inlineSourceMap = true;
config.inlineSourceMap を false にすると、一時レポートにソースマップが含まれなくなった。このため、ts-jest がソースマップを生成していると仮説を立てた。また、react-scripts@2.xの環境では Babel7 がTypeScriptのトランスパイルをサポートするため、ts-jest が存在しないことが判った。
nyc が不要になった
ts-jest をインストールして一時レポートを再び生成すると、依然ソースマップが含まれていないものの、ステートメントなど各種の範囲が正しく記録されていた。レポート形式の指定 で実施した以下の設定を削除し、HTMLレポートも出力して内容を確認すると、期待通りのハイライトが行われていた。
"jest": {
"coverageReporters": [
"json"
]
}
カバレッジの変換
nyc を使うことなく期待通りのレポートが得られた理由を調査した。以下は、一時レポートに書き込みを行う前段のコードである。
var _sourceMapStore$trans = _this._sourceMapStore.transformCoverage(
_this._coverageMap
);
const map = _sourceMapStore$trans.map,
sourceFinder = _sourceMapStore$trans.sourceFinder;
(略)
reporter.write(map, sourceFinder && {sourceFinder: sourceFinder});
reporter.write は、カバレッジを格納した map を内部のコードへ渡し、一時レポートへ書き込むラッパー関数である。react-scripts@1.xの環境では、map 内にソースマップを格納した inputSourceMap というオブジェクトが含まれており、これが一時レポートに書き込まれる2。
_this._sourceMapStore.transformCoverage は、JavaScriptにトランスパイルされたコードに対するカバレッジを格納した _this._coverageMap を元のTypeScriptコードに対するカバレッジへ変換する関数である。
react-scripts@2.xの環境で ts-jest を用いた場合は、_this._coverageMap に inputSourceMap が含まれ、期待通りのレポートが得られる。他方、Babel7 を用いた場合は、inputSourceMap が含まれず、期待通りのレポートが得られない。このため、カバレッジを正しく変換するには inputSourceMap が必要であると考えられる。
カバレッジの変換(内部コード)
以下は、前述した _this._sourceMapStore.transformCoverage の内部コードである。
MapStore.prototype.transformCoverage = function (coverageMap) {
(略)
if (Object.keys(this.data).length === 0) {
return {
map: coverageMap,
sourceFinder: sourceFinder
};
}
mappedCoverage = transformer.create(function (filePath) {
(略)
}).transform(coverageMap);
return {
map: mappedCoverage,
sourceFinder: sourceFinder
};
};
ts-jest を用いた場合は、末尾の return まで処理が進み、変換された情報が返される。他方、Babel7 を用いた場合は、中間の return で変換前の情報が返される。このため、inputSourceMap の有無はカバレッジ変換の正誤ではなく、変換自体の有無に関係することが判った。
原因
Babel7 を用いたことでソースマップを含まないカバレッジが生成される。このため、カバレッジがTypeScript用に変換されず、TypeScriptコードと対応しないレポートが生成される。
解決策
react-scripts@2.xを用いて、正しいカバレッジレポートを生成するTypeScript環境の作成手順を以下に示す。
Reactプロジェクトの作成
$ create-react-app fix_typescript --typescript
$ cd fix_typescript
$ yarn eject
$ yarn add -D ts-jest
package.json
scriptsの追加
以下の設定を追加する。
"scripts": {
"coverage": "rm -rf coverage && yarn test --coverage"
}
jest.transformの修正
設定を以下の通りに修正する。
"jest": {
"transform": {
"^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest",
"^.+\\.tsx?$": "ts-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
}
}
tsconfig.json
compilerOptions.targetの修正
es2015の記法を用いるため、設定を以下の通りに修正する。
"compilerOptions": {
"target": "es2015"
}
周辺ファイルの修正
IstanbulJS - レポート改善 - ソースコード修正、あるいは IstanbulJS - レポート改善 ver.2 - ソースコード修正 に従って周辺ファイルを修正する。
🚨 #前提 に記載の手順はスキップすること。
検証
修正を実施後に再度生成したレポートが、定義したハイライトのルールに従っているか検証する。ここでは、IstanbulJS - レポート改善 - ソースコード修正 のルールで検証を行なった。
TypeScript - react-scripts@2.xのテンプレートを使用
👍 定義したルールに従ってハイライトされている。
まとめ
周辺ファイルを修正する前に ts-jest をインストールすることで、正しいカバレッジレポートの生成を実現した。最終的な処理の流れを以下に示す。
-
Jest:
.tsxファイルをトランスパイルするため、ts-jestを呼び出す。 - ts-jest: TypeScriptコードをJavaScriptへトランスパイルする。
-
Jest: トランスパイルされたJavaScriptコードにてテストを実行し、得られたカバレッジをTypeScriptコード用に変換して
IstanbulJSを呼び出す。 - IstanbulJS: HTMLレポートを生成する。

