前回はistanbulでinstrumentしたAngularアプリに対してPlaywrightテストをし、カバレッジを計測しました。
しかしJestカバレッジと上手くマージ出来ない課題が残りました。今回はPlaywrightカバレッジ計測方法をV8にし、JestもV8でカバレッジ計測することで、お互いのカバレッジが正常にマージされるか試してみます。
環境
・Node.js 22.0.0
・angular 18.2.7
・playwright 1.48.1
・jest 29.7.0
事前準備
説明の為のAngularプロジェクト。前回と同じくpage1とpage2へ遷移、各ページでボタンを押すとテキストが表示されるだけのシンプルなものです。
playwrightだけでカバレッジ100%にしてしまうとJestカバレッジとマージ出来ているかが分からないので、page2.component.tsのボタンは押さないようにテストケースを記述し、故意にカバレッジを下げています。
=============================== Coverage summary ===============================
Statements : 97.01% ( 65/67 )
Branches : 75% ( 3/4 )
Functions : 60% ( 3/5 )
Lines : 97.01% ( 65/67 )
================================================================================
---------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files | 97.01 | 75 | 60 | 97.01 |
src | 100 | 100 | 100 | 100 |
main.ts | 100 | 100 | 100 | 100 |
src/app | 100 | 100 | 100 | 100 |
app.component.ts | 100 | 100 | 100 | 100 |
app.config.ts | 100 | 100 | 100 | 100 |
app.routes.ts | 100 | 100 | 100 | 100 |
src/app/page1 | 100 | 100 | 100 | 100 |
page1.component.ts | 100 | 100 | 100 | 100 |
src/app/page2 | 87.5 | 0 | 0 | 87.5 |
page2.component.ts | 87.5 | 0 | 0 | 87.5 | 14-15
---------------------|---------|----------|---------|---------|-------------------
Playwrightカバレッジ(V8)を計測
2017年、chromeのJavascriptエンジン「V8」にカバレッジ取得機能が実装されました。
chromeを使えばistanbulを使ってアプリケーションコードにinstrumentをしなくても、どこのコードが実行されたか分かる訳ですね。playwright側もV8のカバレッジ機能にアクセスするAPIを持っていて親和性があります。ブラウザメモリに溜まったカバレッジデータはv8-to-istanbulモジュールでistanbulフォーマットファイルに変換できる為、前回使用したistanbulのcliであるnycコマンドで取り回し(レポート出力やマージ)がしやすいです。
V8のカバレッジ機能で計測する場合も、playwright実行中はテストケース毎にブラウザが再起動することは変わらないので、ブラウザメモリ上に溜まったカバレッジをテストケースの都度HDDに保存する必要があります。
前回と同じようにtest関数を拡張してカバレッジデータをファイルとしてダウンロードするようにします。
import { expect, test as testBase } from '@playwright/test';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import v8toIstanbul from 'v8-to-istanbul';
// カバレッジデータファイル出力先
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
if (!fs.existsSync(istanbulCLIOutput)) {
fs.mkdirSync(istanbulCLIOutput);
}
// カバレッジデータファイル名ランダムパート
const generateUUID = () => crypto.randomBytes(16).toString('hex');
// Extend playwright's test function
const test = testBase.extend<{ autoTestFixture: any }>({
autoTestFixture: [
async ({ page, request }, use) => {
// chromeの場合はV8カバレッジ計測
const isChromium = test.info().project.name === 'chromium';
if (isChromium) {
await page.coverage.startJSCoverage({
resetOnNavigation: false,
});
}
// 1testケース実行
await use();
// 1testケースが終了
if (isChromium) {
// カバレッジ計測終了
const coverages = await page.coverage.stopJSCoverage();
for (const entry of coverages) {
// 対象のソースを選別(この例ではバンドルされたmain.jsのみ対象)
// lazyloadモジュールがある場合、リモート配備したoutput-hashingのjsの場合は
// 対象ファイルを増やす為にここをアレンジしてください
if (!entry.url.endsWith('main.js')) {
continue;
}
console.log('entry.url', entry.url);
// 対象ソースのソースマップをGET(Typescriptに戻す為)
// 毎回フェッチせずにキャッシュすれば高速化可能
const sourceMapPms = await request.get(entry.url + '.map');
const sourceMap = await sourceMapPms.json();
// sourceMap.sourceRoot = './';
// Typescriptに変換しつつ、V8カバレッジデータをistanbul形式に変換
const converter = v8toIstanbul('', 0, {
source: entry.source,
sourceMap: { sourcemap: sourceMap },
});
await converter.load();
converter.applyCoverage(entry.functions);
// istanbulフォーマットカバレッジファイルを.nyc_output/へ出力
const coverageJson = JSON.stringify(converter.toIstanbul());
fs.writeFileSync(
path.join(
istanbulCLIOutput,
`playwright_coverage_${generateUUID()}.json`
),
coverageJson
);
}
}
},
{
scope: 'test',
auto: true,
},
],
});
export { expect, test };
次にテストケースを記述するspec.tsで、拡張したtest関数を使うように変更します。
// import { test, expect } from '@playwright/test';
import { test, expect } from './base-fixture';
playwrightテストを実行すると、
% npm run e2e
> ng-playwright-coverage-v8@0.0.0 e2e
> rimraf .nyc_output && npx playwright test --project chromium
Running 1 test using 1 worker
[chromium] › example.spec.ts:4:5 › page1 test
entry.url http://localhost:4200/main.js
1 passed (1.5s)
To open last HTML report run:
npx playwright show-report
testケース分のカバレッジデータファイルが.nyc_outputに出力されました。
% ls .nyc_output
playwright_coverage_c2f615ca3476335b67adf845d4199bd2.json
v8-to-istanbulでistanbulフォーマットに変換していて、nycコマンドのデフォルトターゲットである.nyc_outputディレクトリに出力しました。このままnyc reportでレポートを出力してみます。
% npm run e2e-report
> ng-playwright-coverage-v8@0.0.0 e2e-report
> nyc report
冒頭のPage2Component#doSomethingOnPage2()だけ実行されていないレポートが正しく出力されました。
前回のwebpack設定を追加してistanbulでinstrumentして・・・という作業が無くなって簡単にカバレッジレポートを出力することが出来ました。
続いて、PlaywrightでテストしなかったPage2Component#doSomethingOnPage2()をJestでテストして、カバレッジが100%になるようにしてみます。
Jestカバレッジ(V8)を計測
まずはUTツールをkarma + jasminからjestに変更します。
(そろそろデフォルトをJestにして欲しいですね -_-)
先人達が移行手順を残してくれているので参考にします。
Jestインストール中のインタラクションでV8を選んでいるのでjest.config.tsは以下のようになり、jestのカバレッジはV8形式で計測されます。
import type { Config } from 'jest';
const config: Config = {
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
また、Jest内部ではv8-to-istanbulが使われており、出力されるカバレッジデータファイルはistanbulフォーマットになります。先ほどplaywrightでも同じようにV8で計測、istanbulフォーマットで出力、にしたので丁度いいですね。nycコマンドで扱い易くなります。
src/app/page2/page2.component.spec.tsを作成して、先ほどPlaywrightでテストしなかったメソッドのJestテストコードを書いてみます。(画面系テストをJestでやるケースは減少していると思いますが便宜上)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Page2Component } from './page2.component';
describe('Page2Component', () => {
let component: Page2Component;
let fixture: ComponentFixture<Page2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Page2Component]
})
.compileComponents();
fixture = TestBed.createComponent(Page2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// PlaywrightでテストしなかったメソッドのJestテスト
it('should call doSomethingOnPage2', () => {
component.doSomethingOnPage2();
expect(component.message).toEqual('something on page2 done.');
});
});
jest実行してみます。page2.component用のテストコードしか書いていないのでpage1.componentなど他ソースは対象分母にならず、100%になりました。
% npm test
> ng-playwright-coverage-v8@0.0.0 test
> jest --coverage
(node:41086) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
PASS src/app/page2/page2.component.spec.ts
Page2Component
✓ should create (27 ms)
✓ should call doSomethingOnPage2 (2 ms)
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
page2.component.html | 100 | 100 | 100 | 100 |
page2.component.ts | 100 | 100 | 100 | 100 |
----------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.303 s
Ran all test suites.
playwrightで担保出来なかったpage2.componentのカバレッジはjestで担保ができたので、playwrightのカバレッジとマージしてみます。
PlaywrightカバレッジとJestカバレッジをマージ
nycコマンドの設定ファイル「.nycrc」でreporterに”json”を指定している場合、playwright、jest各々のnyc report結果ディレクトリには「coverage-final.json」というファイルが出来ています。このファイルには全テストケースのV8カバレッジデータがistanbul形式で保存されています。
{
"/Users/nakanishishingo/src/NgPlaywrightCoverageV8/src/main.ts": {
"path": "/Users/nakanishishingo/src/NgPlaywrightCoverageV8/src/main.ts",
"all": false,
"statementMap": {
"0": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 65
}
},
"1": {
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 2,
"column": 45
}
},
(snip)
"5": {
"start": {
"line": 6,
"column": 0
},
"end": {
"line": 6,
"column": 38
}
}
},
"s": {
"0": 1,
"1": 1,
"2": 1,
"3": 1,
"4": 1,
"5": 1
},
"branchMap": {},
"b": {},
"fnMap": {},
"f": {}
}
}
これらcoverage-final.jsonをリネームして一つのディレクトリに配置し、nyc reportの対象ディレクトリとすることでマージされたレポートが出来上がります。前処理、後処理色々あるのでnpmスクリプト化します。
{
"scripts": {
"premerge-report": "rimraf covwork && mkdir covwork && cpy coverage-jest/coverage-final.json covwork --rename=coverage-jest.json --flat && cpy coverage-e2e/coverage-final.json covwork --rename=coverage-e2e.json --flat ",
"merge-report": " nyc report -t covwork --report-dir coverage-merged",
"postmerge-report": "rimraf covwork"
実行してみると、
% npm run merge-report
> ng-playwright-coverage-v8@0.0.0 premerge-report
> rimraf covwork && mkdir covwork && cpy coverage-jest/coverage-final.json covwork --rename=coverage-jest.json --flat && cpy coverage-e2e/coverage-final.json covwork --rename=coverage-e2e.json --flat
> ng-playwright-coverage-v8@0.0.0 merge-report
> nyc report -t covwork --report-dir coverage-merged
=============================== Coverage summary ===============================
Statements : 100% ( 67/67 )
Branches : 100% ( 6/6 )
Functions : 100% ( 6/6 )
Lines : 100% ( 67/67 )
================================================================================
---------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
src | 100 | 100 | 100 | 100 |
main.ts | 100 | 100 | 100 | 100 |
src/app | 100 | 100 | 100 | 100 |
app.component.ts | 100 | 100 | 100 | 100 |
app.config.ts | 100 | 100 | 100 | 100 |
app.routes.ts | 100 | 100 | 100 | 100 |
src/app/page1 | 100 | 100 | 100 | 100 |
page1.component.ts | 100 | 100 | 100 | 100 |
src/app/page2 | 100 | 100 | 100 | 100 |
page2.component.ts | 100 | 100 | 100 | 100 |
---------------------|---------|----------|---------|---------|-------------------
> ng-playwright-coverage-v8@0.0.0 postmerge-report
> rimraf covwork
playwrightだけでは87.5%だったpage2.component.tsのカバレッジが、jestテストカバレッジとマージすることによって100%になりました。全体カバレッジも97.01%から100%になりました。
まとめ
前回のistanbulでの計測ではバンドルされたjsと、単体ソースに対するJestテストのソース解釈が異なって上手くマージすることが出来ませんでした。
(webpackバンドルによってinstrumentされたソース定義が変わってしまうことが原因でistanbulのせいではありません)
今回はV8での計測にすることでソース解釈が等しくなり正確にマージすることができました。
E2E、UTコードを書いておくことでデグレ起因バグを未然に検知しやすくなります。ですが、カバレッジが低ければその効果も下がってしまいます。
枕を高くして眠る為に高カバレッジをキープしていきたいですね。