Edited at

AtomでJavaScriptのテストカバレッジを可視化するパッケージを作って公開した話

More than 1 year has passed since last update.

個人開発 Advent Calendar 2017の17日目の記事です。


作ったもの

JavaScriptのテストカバレッジをAtomで表示するパッケージを作りました。

coverage-markers

coverage_markers_package.png

こちらが実際に動かした様子です。テストが網羅されている行は緑色、網羅されていない行には赤色のマーカーが表示されます。

パッケージには以下の特徴があります。


テストカバレッジ表示はLCOV形式に対応

$ nyc --reporter=lcov --reporter=text npm run test

nycistanbulなどのカバレッジ計測ツールではLCOVフォーマットでのカバレッジ出力に対応しており、本パッケージではその出力されたファイル(lcov.info)を利用しています。

coverage_nyc.png

もともとnycやistanbulには結果をHTMLで表示する機能があり、今回はその一部(Statement Coverage)をAtomで簡単に表示できるようにしました。


カバレッジファイルを監視して変更の度に結果をエディタに反映

usage.gif

テストを実行してカバレッジファイルに変更があれば、データを読み直して結果をエディタに反映するようになっています。


開発のきっかけ

今年6月中旬にAWS Summit Tokyo 2017でのt_wadaさんのTestable Lambdaの講演動画を見ました。その中で紹介されていたemacs向けのカバレッジ可視化ツール「coverlay.el」が非常に魅力的に思えました。

なぜなら、私の所属するチームでは機能追加と共にユニットテストを書く文化があり、普段からテストを書く機会が多かったのでカバレッジを参考にしながらテストを書けたら便利だと考えたからです。

それを実現するために普段使っているAtomで使えるカバレッジ可視化ツールを作ることにしました。1


動作の仕組み

実際にカバレッジを表示するために以下の処理を行っています。


  1. プロジェクトディレクトリからカバレッジファイルlcov.infoを検索する

  2. カバレッジファイルからカバレッジデータを読み込む

  3. カバレッジデータをJSONに変換

  4. 各エディタのガターにマーカーをレンダリングする

  5. カバレッジファイルに変更があれば、2.に戻って繰り返し

本稿では特にメインの処理である、1、3、4、5について紹介します。


ファイル検索

async function findCoverageFilePath(projectPath) {

try {
// 検索対象外のディレクトリを指定しておく
const ignore = [`${projectPath}/**/node_modules/**`];
const matchPaths = await glob.globAsync(`${projectPath}/**/lcov.info`, { ignore, dot: false });

// lcov.infoがヒットしたらファイルパスを返す
if (matchPaths.length > 0) {
return matchPaths[0];
}

throw new Error('coverage file path could not be found.');
} catch (error) {
throw new Error(error.message);
}
}

nycやistanbulなどのカバレッジ計測ツールを使用すると、lcov.infoというカバレッジデータを記録したファイルを出力します。本パッケージではプロジェクトディレクトリ内のlcov.infoをファイル検索し読み込んで利用しています。この処理ではglobというファイル名のパターンマッチングができるモジュールを使用しています。

isaacs/node-glob: glob functionality for node.js


カバレッジデータ変換

lcov.infoに記録されたカバレッジデータを読み込んで、JSONに変換しパッケージ内で取り扱えるようにしています。この処理の実現にはlcov.jsというモジュールを使用しました。

TN:

SF:/test-project/index.js
DA:16,0
DA:17,0
DA:22,0
DA:29,0
DA:33,0
LF:7
LH:0
BRF:0
BRH:0
end_of_record

lcov.infoのカバレッジデータ

[

{
"sourceFile": "/test-project/index.js",
"branches": {
"found": 0,
"hit": 0,
"data": []
},
"functions": {
"found": 0,
"hit": 0,
"data": []
},
"lines": {
"found": 7,
"hit": 0,
"data": [
{
"lineNumber": 16,
"executionCount": 0,
"checksum": ""
},
{
"lineNumber": 17,
"executionCount": 0,
"checksum": ""
},
{
"lineNumber": 22,
"executionCount": 0,
"checksum": ""
},
{
"lineNumber": 29,
"executionCount": 0,
"checksum": ""
},
{
"lineNumber": 33,
"executionCount": 0,
"checksum": ""
}
]
}
}
]

JSON変換後は各ファイルごとにカバレッジ情報がまとめられます。この情報をもとにエディタにマーカーをレンダリングします。

function parseCoverageFile(coverageFile) {

try {
const report = Report.fromCoverage(coverageFile);
const coverage = report.toJSON();
return coverage.records;
} catch (error) {
throw new Error('coverage file could not be parsed.');
}
}

こちらがカバレッジデータをJSONに変換している部分です。モジュールを使用することで数行書くだけで処理ができました。

cedx/lcov.js: Parse and format LCOV coverage reports, in JavaScript.


マーカーのレンダリング

import { Range } from 'atom';

export default class Marker {
constructor(editor) {
this.editor = editor;
}

// テストがカバーされていない行に適用
addTestUncovered(line) {
const range = new Range([line - 1, 0], [line - 1, 0]);
const marker = this.editor.markBufferRange(range, { invalidate: 'never' });
// CSSでクラスを当てることでマーカーを表示
this.editor.decorateMarker(marker, { type: 'line-number', class: 'line-number-red' });
}

// テストがカバーされている行に適用
addTestCovered(line) {
const range = new Range([line - 1, 0], [line - 1, 0]);
const marker = this.editor.markBufferRange(range, { invalidate: 'never' });
// CSSでクラスを当てることでマーカーを表
this.editor.decorateMarker(marker, { type: 'line-number', class: 'line-number-green' });
}

remove() {
const markers = this.editor.getMarkers();
if (markers.length > 0) {
markers.forEach((marker) => {
marker.destroy();
});
}
}
}

マーカーのレンダリング処理はAtom APIのTextEditorインスタンスdecorateMarkerメソッドを呼び出しています。実際は行ごとに処理が行われるので、JSONに変換されたカバレッジデータを元にループで回してレンダリングしています。


ファイル監視

import chokidar from 'chokidar';

// chokidarインスタンスを返して、実際のファイル変更検知後の処理は別の場所で行う
function createWatcher(coverageFilePath) {
const options = {
awaitWriteFinish: { // ファイル変更が完了したと判断する
stabilityThreshold: 2000, // ファイル変更が完了したと判断する秒数
pollInterval: 100, // ファイル変更が完了したかを確認する間隔
},
};

const watcher = chokidar
.watch(coverageFilePath, options);

return watcher;
}

const watcher = createWatcher('lcov.info');
watcher.on('change', async () => {
// ファイルが変更されたら処理
});

テストを実行してカバレッジが変更された結果を反映するためにファイル監視機能を導入しています。この処理はchokidarというモジュールを使用して実装しています。

ただ1つ難点があり、モジュール内部でfseventsというmacOSのネイティブAPIにアクセス処理があるため、本パッケージをインストールやアップデートしたときに必ずAtomをリビルドしないといけない制約があります。なんとか回避するために現在他のパッケージに置き換えられないかを調べています。

paulmillr/chokidar: A neat wrapper around node.js fs.watch / fs.watchFile / fsevents.


開発のポイント

はじめてのAtomパッケージ開発だったのでお役立ち情報をまとめておきたいと思います。


パッケージ作成

Atomにはパッケージの雛形を作成するためのpackage-generatorというパッケージが提供されています。基本的にはこれで雛形を作成し、それに処理が足していくのが無難だと思います。開発言語に関しては以前はCoffeeScriptでしたが、JavaScript(ES6)が使用できます。

その他パッケージ作成の参考になるページを載せます。


ビルド・テスト

package-generatorで雛形を作成した場合はJasmineというテスティングフレームワークがデフォルトで含まれています。これをmochaやchaiなど好みのフレームワークを使用する場合はテストランナーを作成する必要があります。


今後の展望

coverage_percentage.png

coverage_percentage002.png

カバレッジのパーセンテージ表示機能を追加するために実現する方法を探っているところです。


さいごに

開発したAtom向けカバレッジ可視化ツールの概要と動作の仕組みを簡単ですが紹介しました。当初目指していたテストカバレッジをエディタに可視化するという目的は達成できたので満足しています。

自分がエディタに欲しいと思った機能をパッケージ開発という形で実現できるのが、Atomのパッケージエコシステムの魅力だと思うので機会があれば挑戦してみるのはいかがでしょうか。


リポジトリ

kentaro-m/coverage-markers: Displays JavaScript code coverage on gutter of editor in Atom.





  1. Atom向けのカバレッジ可視化ツールはすでに存在していましたが、開発が止まっていたり、上手く動かなかったので勉強も兼ねて作ることにしました。