LoginSignup
5
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-05

🚨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しかカウントされない)。 

5
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
10