🚨react-scripts@2.xを使ってTypeScript環境を構築する場合は、以下の手順は不要である。
概要
JestにてTypeScriptで書かれたReactアプリケーションのカバレッジを計測すると、内部でJavaScriptにトランスパイルされたソースコードに対するレポートが出力される。これをTypeScriptで書かれたソースコードに対応させるための対策を記載する。
従来の対策
Jest(から呼び出されるIstanbul)が出力するJSON形式のレポートにはソースマップが含まれる。これを用いてTypeScriptのソースコードにマッピング(Remap)するため、これまではremap-istanbulというツールが利用されてきた。自分の環境では今も正常に動作するが、以下の理由から別の対策を検討した。
- 内部で使用しているIstanbulのバージョンがDeprecatedとなっている0.xである。
- 最新版のIstanbulがRemapをサポートしているため、remap-istanbulを用いる必要が無い。
- 古いバージョンのIstanbulを用いる必要がある場合のみremap-istanbulを使うよう、開発者がコメントしている。1
今回の対策
Istanbulの開発元が公開しているnycというツールを用いる。従来通りにJSON形式のレポートを取得したのち、nyc経由でIstanbulにてRemapを行う。
環境
- macOS 10.14.2
- Node v11.1.0
- React 16.7.0
- create-react-app 2.1.2
$ create-react-app correct-coverage --scripts-version=react-scripts-ts
$ cd correct-coverage
$ yarn add -D nyc
Jest
レポート形式の指定
Jest実行後のレポートとしてcoverage-final.json
のみ利用するため、不要なレポートを出力しないようレポートの種類を指定する。
"jest": {
"coverageReporters": [
"json"
]
}
tsconfig
コンパイルの設定
compilerOptionsのtargetを、TypeScriptソースコードで用いている記法に合わせる。今回はsrc/App.tsx
がes2015で書かれているため、同様にtargetをes2015とした。
"compilerOptions": {
"target": "es2015"
}
コンソール上への出力を抑制
レポート形式にtext
text-summary
のいずれも指定されない場合、Jestは自動的にtext-summary
を追加してレポートを出力する。これを抑制するため、当該処理のコードを下記のようにコメントアウトする。
// if (
// !_this._globalConfig.useStderr &&
// coverageReporters.length &&
// coverageReporters.indexOf('text') === -1)
// {
// coverageReporters = coverageReporters.concat(['text-summary']);
// }
nyc
設定ファイルの作成
プロジェクトのルートディレクトリに.nycrc
を作成し、以下の設定を記述する。
{
"reporter": [
"text-summary",
"json",
"html"
],
"temp-dir": "./coverage"
}
package.json
scriptsの追加
最終的なカバレッジは、Jestとnycを用いて以下の手順で取得する。
- Jest: テストを実行 → カバレッジを計測 → レポートを出力
- nyc: レポートをRemap
それぞれコマンドを1回ずつ実行することになるが、簡略化のためpackage.jsonのscriptsにコマンドをまとめる。古いレポートが残らないよう、coverageディレクトリの削除も組み込んだ。
"scripts": {
"coverage": "rm -rf coverage && yarn test --coverage && nyc report"
}
動作確認
カバレッジ計測
コンソール上、HTML形式ともにTypeScriptソースコードに対応したレポートが出力される。JSON形式のレポートは長いためここに記載しないが、同様に正しく出力されていた。
$ yarn coverage
yarn run v1.10.1
$ rm -rf coverage && yarn test --coverage && nyc report
$ node scripts/test.js --env=jsdom --coverage
PASS src/App.test.tsx
✓ renders without crashing (22ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.618s
Ran all test suites.
=============================== Coverage summary ===============================
Statements : 100% ( 5/5 )
Branches : 100% ( 0/0 )
Functions : 100% ( 1/1 )
Lines : 100% ( 5/5 )
================================================================================
✨ Done in 3.73s.
付録(NGパターン)
Remapなしのカバレッジ
#環境を実施後にカバレッジを計測すると、以下のようなレポートが出力される。
$ yarn test --coverage
yarn run v1.10.1
$ node scripts/test.js --env=jsdom --coverage
PASS src/App.test.tsx
✓ renders without crashing (30ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.796s
Ran all test suites.
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
App.tsx | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
✨ Done in 3.40s.
Statements
Branches
Functions
Lines
の値がいずれも誤っており、1x
と書かれた場所も滅茶苦茶である。
コンパイル設定なしのカバレッジ
※ #レポート形式の指定を実施せずに検証する。
#コンパイルの設定を実施せずにカバレッジを計測すると、以下のようなレポートが出力される。
yarn test --coverage
yarn run v1.10.1
$ node scripts/test.js --env=jsdom --coverage
PASS src/App.test.tsx
✓ renders without crashing (23ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.491s, estimated 2s
Ran all test suites.
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
App.tsx | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
✨ Done in 2.98s.
Statements
Functions
Lines
の値がいずれも誤っており、1x
と書かれた場所も部分的におかしい。
考察
tsconfig.test.json
はtsconfig.json
をオーバーライドしているため、tsconfig.test.json
でcompilerOptions.target
を指定しなければtsconfig.json
で指定している"es5"
が用いられる。
es5にトランスパイルされたソースコードを確認する。以下の行にfilePath.indexOf("src/App.tsx") !== -1
を条件としたブレークポイントをセットし、デバッグする。
var tsTranspiled = tsc.transpileModule(src, {
compilerOptions: compilerOptions,
fileName: filePath,
});
▶︎ var tsJestConfig = utils_1.getTSJestConfig(jestConfig.globals);
tsTranspiled.outputText
には以下のようなトランスパイル済みのJavaScriptコードが格納される。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = require("react");
require("./App.css");
var logo_svg_1 = require("./logo.svg");
var App = /** @class */ (function (_super) {
tslib_1.__extends(App, _super);
function App() {
return _super !== null && _super.apply(this, arguments) || this;
}
App.prototype.render = function () {
return (React.createElement("div", { className: "App" },
React.createElement("header", { className: "App-header" },
React.createElement("img", { src: logo_svg_1.default, className: "App-logo", alt: "logo" }),
React.createElement("h1", { className: "App-title" }, "Welcome to React")),
React.createElement("p", { className: "App-intro" },
"To get started, edit ",
React.createElement("code", null, "src/App.tsx"),
" and save to reload.")));
};
return App;
}(React.Component));
exports.default = App;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQXBwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiQXBwLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2QkFBK0I7QUFDL0IscUJBQW1CO0FBRW5CLHVDQUE4QjtBQUU5QjtJQUFrQiwrQkFBZTtJQUFqQzs7SUFjQSxDQUFDO0lBYlEsb0JBQU0sR0FBYjtRQUNFLE9BQU8sQ0FDTCw2QkFBSyxTQUFTLEVBQUMsS0FBSztZQUNsQixnQ0FBUSxTQUFTLEVBQUMsWUFBWTtnQkFDNUIsNkJBQUssR0FBRyxFQUFFLGtCQUFJLEVBQUUsU0FBUyxFQUFDLFVBQVUsRUFBQyxHQUFHLEVBQUMsTUFBTSxHQUFHO2dCQUNsRCw0QkFBSSxTQUFTLEVBQUMsV0FBVyx1QkFBc0IsQ0FDeEM7WUFDVCwyQkFBRyxTQUFTLEVBQUMsV0FBVzs7Z0JBQ0QsZ0RBQXdCO3VDQUMzQyxDQUNBLENBQ1AsQ0FBQztJQUNKLENBQUM7SUFDSCxVQUFDO0FBQUQsQ0FBQyxBQWRELENBQWtCLEtBQUssQ0FBQyxTQUFTLEdBY2hDO0FBRUQsa0JBQWUsR0FBRyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnO1xuaW1wb3J0ICcuL0FwcC5jc3MnO1xuXG5pbXBvcnQgbG9nbyBmcm9tICcuL2xvZ28uc3ZnJztcblxuY2xhc3MgQXBwIGV4dGVuZHMgUmVhY3QuQ29tcG9uZW50IHtcbiAgcHVibGljIHJlbmRlcigpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPGRpdiBjbGFzc05hbWU9XCJBcHBcIj5cbiAgICAgICAgPGhlYWRlciBjbGFzc05hbWU9XCJBcHAtaGVhZGVyXCI+XG4gICAgICAgICAgPGltZyBzcmM9e2xvZ299IGNsYXNzTmFtZT1cIkFwcC1sb2dvXCIgYWx0PVwibG9nb1wiIC8+XG4gICAgICAgICAgPGgxIGNsYXNzTmFtZT1cIkFwcC10aXRsZVwiPldlbGNvbWUgdG8gUmVhY3Q8L2gxPlxuICAgICAgICA8L2hlYWRlcj5cbiAgICAgICAgPHAgY2xhc3NOYW1lPVwiQXBwLWludHJvXCI+XG4gICAgICAgICAgVG8gZ2V0IHN0YXJ0ZWQsIGVkaXQgPGNvZGU+c3JjL0FwcC50c3g8L2NvZGU+IGFuZCBzYXZlIHRvIHJlbG9hZC5cbiAgICAgICAgPC9wPlxuICAgICAgPC9kaXY+XG4gICAgKTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBBcHA7XG4iXX0=
比較のため、Remapの基になるcoverage/coverage-final.json
をJestで出力する。
$ yarn test --coverage
yarn run v1.10.1
$ node scripts/test.js --env=jsdom --coverage
PASS src/App.test.tsx
✓ renders without crashing (24ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.492s
Ran all test suites.
✨ Done in 2.71s.
"statementMap": {
"0": {
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 2,
"column": 62
}
},
上記の抜粋部分は、0番目のステートメントの範囲が2行目の0文字目から2行目の62文字目までであることを表している。2
比較: HTML版レポート / JSON版レポート / TypeScriptソースコード / トランスパイル済みJavaScriptソースコード
HTML版レポートに記載されたStatements
Branches
Functions
Lines
をそれぞれ他のレポート、およびソースコードと比較して妥当性を検証する。
Statements
JSON版レポートのstatementMap
に対応する。今回の例では全ステートメントの開始と終了が同じ行のため、jq
にて開始位置の行番号を抽出すると以下のような結果になる。
$ cat coverage/coverage-final.json | jq '.["/Users/thara/Desktop/correct-coverage/src/App.tsx"].statementMap[]' | jq '.start.line'
2
3
4
5
6
7
8
10
12
13
22
24
これはトランスパイル済みソースコードにおけるステートメントの位置と合致する。
HTML版レポートにてステートメントと判断された位置(1x
と記載されている行)も同様に、JSON版レポートのstatementMap
に合致する。
Branches
JSON版レポートのbranchMap
に対応し、トランスパイル済みソースコードにおける10行目の3箇所に合致する。
Functions
JSON版レポートのfnMap
に対応し、トランスパイル済みソースコードにおけるfunction
キーワードの3箇所に合致する。
Lines
JSON版レポートに対応する情報はない。1行ごとに1ステートメントになるようトランスパイルされるため、Statements
と同一である。3
-
https://github.com/SitePen/remap-istanbul/issues/177#issuecomment-438677707 ↩
-
lineは1-based、columnは0-basedである。 ↩
-
基本的にライン数(行数)とステートメント数は同一だが、同じ行にステートメントが複数存在する場合に差異が生まれる(ステートメントは個数分カウントされるのに対して、ラインは1しかカウントされない)。 ↩