angular coverage

AngularアプリをPlaywright、 Jestテストし、カバレッジをマージする(V8編)

投稿日:2024年11月3日

前回は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()だけ実行されていないレポートが正しく出力されました。

coverage-e2e/index.html

前回の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.
coverage-jest/lcov-report/index.html

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
coverage-merged/index.html

playwrightだけでは87.5%だったpage2.component.tsのカバレッジが、jestテストカバレッジとマージすることによって100%になりました。全体カバレッジも97.01%から100%になりました。

まとめ

前回のistanbulでの計測ではバンドルされたjsと、単体ソースに対するJestテストのソース解釈が異なって上手くマージすることが出来ませんでした。

(webpackバンドルによってinstrumentされたソース定義が変わってしまうことが原因でistanbulのせいではありません)

今回はV8での計測にすることでソース解釈が等しくなり正確にマージすることができました。

E2E、UTコードを書いておくことでデグレ起因バグを未然に検知しやすくなります。ですが、カバレッジが低ければその効果も下がってしまいます。

枕を高くして眠る為に高カバレッジをキープしていきたいですね。

-angular, coverage

執筆者:

関連記事

compodocでAngularプロジェクトのビジュアルなドキュメントを自動生成する

Java、C、Pythonのドキュメントを自動生成する際にDoxygenを使えばクラス図や呼び出し図、呼び出され図を作れて便利です。 しかしDoxygenはTypescriptには対応しておらず、.t …

Angularのテンプレート評価式にビット演算を使うとTemplate parse errorが発生する

AngularのテンプレートHTMLでビット演算をすることは禁じられているので代替手段を考えます。 目次1 事象2 原因3 対処 事象 CSSクラスをビット演算で切り替えるテンプレートを書きました。c …

ブラウザでRSA暗号化したデータをサーバで復号する(Angular + JSEncrypt、Spring MVC)【後編】

前回の続きです。One IT ThingブラウザでRSA暗号化したデータをサーバで復号する(Angular + JSEncrypt、Spring …https://one-it-thing.com …

正規表現結果をHTMLコード化してマッチ部分をハイライト表示

やりたかったのは「Rubular」が提供してくれるマッチした部分をハイライトしてくれる機能。 https://rubular.com/ ちょっとしたツールにこういう機能を入れようと思ったのですが、ニー …

AngularアプリをPlaywrightテストしてカバレッジを計測する(webpack + istanbul編)

Angular17からはデフォルトのバンドルツールがesbuildに変わったこと、Angular & istanbul形式だとjestのistanbul形式と正確にマージ出来ないことから、An …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。