TypeScript
Jest
React
nyc

[React + TypeScript] カバレッジレポートの取得方法

🚨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のみ利用するため、不要なレポートを出力しないようレポートの種類を指定する。


package.json

  "jest": {

"coverageReporters": [
"json"
]
}


tsconfig


コンパイルの設定

compilerOptionsのtargetを、TypeScriptソースコードで用いている記法に合わせる。今回はsrc/App.tsxがes2015で書かれているため、同様にtargetをes2015とした。


tsconfig.test.json

  "compilerOptions": {

"target": "es2015"
}


コンソール上への出力を抑制

レポート形式にtext text-summaryのいずれも指定されない場合、Jestは自動的にtext-summaryを追加してレポートを出力する。これを抑制するため、当該処理のコードを下記のようにコメントアウトする。


node_modules/jest-cli/build/reporters/CoverageReporter.js

        // if (

// !_this._globalConfig.useStderr &&
// coverageReporters.length &&
// coverageReporters.indexOf('text') === -1)
// {
// coverageReporters = coverageReporters.concat(['text-summary']);
// }


nyc


設定ファイルの作成

プロジェクトのルートディレクトリに.nycrcを作成し、以下の設定を記述する。


.nycrc

{

"reporter": [
"text-summary",
"json",
"html"
],
"temp-dir": "./coverage"
}


package.json


scriptsの追加

最終的なカバレッジは、Jestとnycを用いて以下の手順で取得する。



  1. Jest: テストを実行 → カバレッジを計測 → レポートを出力


  2. nyc: レポートをRemap

それぞれコマンドを1回ずつ実行することになるが、簡略化のためpackage.jsonのscriptsにコマンドをまとめる。古いレポートが残らないよう、coverageディレクトリの削除も組み込んだ。


package.json

  "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.

Screen Shot 2018-12-06 at 8.13.09.png


付録(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.

Screen Shot 2018-12-06 at 8.33.01.png

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.

Screen Shot 2018-12-06 at 8.52.44.png

Statements Functions Linesの値がいずれも誤っており、1xと書かれた場所も部分的におかしい。


考察

tsconfig.test.jsontsconfig.jsonをオーバーライドしているため、tsconfig.test.jsoncompilerOptions.targetを指定しなければtsconfig.jsonで指定している"es5"が用いられる。

es5にトランスパイルされたソースコードを確認する。以下の行にfilePath.indexOf("src/App.tsx") !== -1を条件としたブレークポイントをセットし、デバッグする。


node_modules/ts-jest/dist/preprocessor.js

    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.


coverage/coverage-final.json(一部抜粋)

        "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

これはトランスパイル済みソースコードにおけるステートメントの位置と合致する。

Screen Shot 2018-12-06 at 11.03.07.png

HTML版レポートにてステートメントと判断された位置(1xと記載されている行)も同様に、JSON版レポートのstatementMapに合致する。

Screen Shot 2018-12-06 at 9.46.57.png


Branches

JSON版レポートのbranchMapに対応し、トランスパイル済みソースコードにおける10行目の3箇所に合致する。

Screen Shot 2018-12-06 at 11.10.46.png


Functions

JSON版レポートのfnMapに対応し、トランスパイル済みソースコードにおけるfunctionキーワードの3箇所に合致する。

Screen Shot 2018-12-06 at 11.18.22.png


Lines

JSON版レポートに対応する情報はない。1行ごとに1ステートメントになるようトランスパイルされるため、Statementsと同一である。3





  1. https://github.com/SitePen/remap-istanbul/issues/177#issuecomment-438677707 



  2. lineは1-based、columnは0-basedである。 



  3. 基本的にライン数(行数)とステートメント数は同一だが、同じ行にステートメントが複数存在する場合に差異が生まれる(ステートメントは個数分カウントされるのに対して、ラインは1しかカウントされない)。