DUICUO

オープンソース | Canyon: JavaScriptコードの品質向上のための包括的なカバレッジ分析ツール

著者について

wr_zhang25 は Ctrip のシニア フロントエンド開発エンジニアで、フロントエンドのコード カバレッジとオープンソース JavaScript に重点を置いています。

Liang 氏は、Ctrip のシニア R&D マネージャー兼品質エキスパートであり、品質エンジニアリングを専門としています。

I. 背景

Istanbul.js は優れた JavaScript コードカバレッジツールであり、主にユニットテストにおけるコードカバレッジ検出とローカルカバレッジレポートの生成に使用されます。しかし、最新のフロントエンド技術と UI 自動テストの発展に伴い、エンドツーエンドテストにおけるコードカバレッジ検出の需要が徐々に高まっており、istanbul.js が提供する機能は不十分であることが明らかになっています。

Ctrip社内では、JavaScriptのコードカバレッジはGitLabの組み込みカバレッジレポート機能を使用して報告されていますが、これは単体テストのカバレッジ収集と概要データの表示のみをサポートしています。Ctripのフロントエンド技術が高度化するにつれ、独自のフロントエンドトラフィック記録プラットフォームを開発し、UI自動化(Flybirds)の再生用に大規模なシミュレータクラスターを導入しました。このシナリオでは、開発者がコードの品質をより深く理解できるように、エンドツーエンドのテストコードカバレッジを収集して表示する必要性が高まっています。

Istanbul.js の従来の機能では、もはや私たちのニーズを満たすことができませんでした。UI 自動化、リアルタイムのカバレッジ集計、そしてカバレッジデータの集約表示といった、フロントエンドからの高並列カバレッジレポートを処理する必要がありました。そこで、エンドツーエンドのテストカバレッジ収集の難しさという問題を解決するため、Istanbul.js 上に Canyon を開発しました。

現在、Ctripの複数の部門がCanyonを使い始めており、継続的インテグレーションパイプラインのビルドフェーズにプローブコードを挿入し、UI自動テストフェーズでカバレッジデータを収集・レポートしています。サーバーは詳細なカバレッジレポートをリアルタイムで生成し、UI自動テストケースの包括的なカバレッジデータメトリクスを提供します。

II. はじめに

Canyonは、シンプルなBabelプラグイン設定により、コードインストルメンテーション、カバレッジレポート、リアルタイムレポート生成を可能にします。そのテクノロジースタックは完全にJavaScriptベースで、Node.js環境のみで実行できるため、導入が容易で、DockerやKubernetesなどのクラウドネイティブ環境にも適しています。

アプリケーションのアーキテクチャは、高頻度かつ大規模なカバレッジデータレポートを処理できるように設計されており、UI自動テストにおける様々なシナリオに対応できます。さらに、Canyonは既存のCI/CDツール(GitLab CIやJenkinsなど)とシームレスに統合されているため、ユーザーは継続的インテグレーションパイプラインで容易に使用できます。

アーキテクチャ図は次のとおりです。

Canyon の主な機能については、以下のセクションで紹介します。

  • コードカバレッジ
  • コードインストルメンテーション
  • テストとレポート
  • カバレッジ集約
  • 報道レポート
  • コードカバレッジの変更
  • React Native カバレッジ収集ソリューション
  • カバレッジ改善優先リスト

III. コードカバレッジ

エンドツーエンドのテストケースをどんどん書いていくと、次のような疑問が湧いてくるかもしれません。もっとテストケースを書く必要があるのか​​?テストにまだ足りないコードは何か?重複したテストケースはあるのか?そんな疑問に直面するのがコードカバレッジです。コードカバレッジの原理は、コードが実行される前にソースコードにコードプローブ(基本的にはコンテキストとカウンター)を挿入し、テストケースが実行されるたびにカウンターがトリガーされるようにすることです。

コードプローブをコードに挿入するプロセスは、コードインストルメンテーションと呼ばれます。インストルメンテーション前のコードは以下のとおりです。

 // add.js function add(a, b) { return a + b } module.exports = { add }

インストルメンテーションとは、コードを解析してすべての関数、文、分岐を見つけ出し、コードにカウンターを挿入することです。上記のコードの場合、インストルメンテーション後、以下の処理が行われます。

 // 这个对象用于计算每个函数和每个语句被执行的次数const c = (window.__coverage__ = { // "f" 记录每个函数被调用的次数f: [0], // "s" 记录每个语句被调用的次数// 我们有3个语句,它们都从0开始s: [0, 0, 0], }) // 第一个语句定义了函数cs[0]++ function add(a, b) { // 函数被调用后是第二个语句cf[0]++ cs[1]++ return a + b } // 第三个语句即将被调用cs[2]++ module.exports = { add }

`add.js` ファイル内のすべてのステートメントと関数がテストによって少なくとも1回は実行されたことを確認したいので、次のようなテストを作成します。

 // add.cy.js const { add } = require('./add') it('adds numbers', () => { expect(add(2, 3)).to.equal(5) })

テストが `add(2, 3)` を呼び出すと、 `add` 関数内のカウンターがインクリメントされ、カバーされるオブジェクトは次のようになります。

 { f: [1], s: [1, 1, 1] }

このテストケースは、各関数と各ステートメントが少なくとも1回実行され、100%のカバレッジを達成しました。しかし、実際のアプリケーションでは、100%のコードカバレッジを達成するには複数のテストが必要になります。

これはカバレッジの基本的な入門です。この背景知識があれば、以降の内容を理解しやすくなります。

IV. コードインストルメンテーション

コード カバレッジの最も重要な側面は、コード インストルメンテーションです。

Istanbul.jsは、JavaScriptコードのインストルメンテーションにおける実績のあるゴールドスタンダードです。Canyonは主にエンドツーエンドテスト向けのソリューションを提供しており、広範な実験検証の結果、現代のフロントエンドエンジニアリングにおけるカバレッジインストルメンテーションはコンパイル時に実行する必要があることが示されています。具体的な理由は、Istanbul.jsのNYCインストルメンテーションツールがネイティブJavaScriptのみをインストルメントできるためです。しかし、フロントエンドテンプレート構文はTypeScript、TSX、Vueなど、常に進化しています。NYCもインストルメンテーションを実行できますが、実際の経験から、直接インストルメンテーションを行うと十分なカバレッジが得られず、インストルメンテーションの対象とすべき関数、ステートメント、または分岐を正確に特定できないことが分かっています。

幸いなことに、調査を通じて、babel-plugin-istanbul、vite-plugin-istanbul(試験的)、swc-plugin-coverage-instrument(試験的)といったプロジェクト向けのインストルメンテーションソリューションを発見しました。これらのソリューションは例外なく、フロントエンドプロジェクトのコンパイルフェーズにおいて、コードがAST(抽象構文木)に解析される適切なタイミングでインストルメンテーションメソッド呼び出しを実行し、関数、ステートメント、分岐のより正確なインストルメンテーションを可能にします。

適用可能なプロジェクトの種類:

プロジェクトの種類

プラン

バニラJavaScript

ニューヨーク

バベル

babel-プラグイン-イスタンブール

ヴィート

vite-plugin-istanbul(実験的)

swc

swc-plugin-coverage-instrument(実験的)

ユーザーはプロジェクトの種類に応じて適切なインストルメンテーションスキームを選択できます。プロジェクトに対応するプラグインをインストールするだけで、コンパイル時にインストルメンテーションが自動的に実行されます。

babel.config.js を例に挙げます。

 module.exports = { plugins: [ [ 'babel-plugin-istanbul', { exclude: ['**/*.spec.js', '**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.jsx'], }, ], ], };

インストルメンテーションが完了すると、コードにいくつかのコードプローブが挿入されます。これらのコードプローブは実行時にカバレッジデータを収集し、Canyonサーバーに報告します。

インストルメンテーションが成功したかどうかを確認するには、コンパイルされた出力で __coverage__ を検索します。見つかった場合は、インストルメンテーションは成功しています。

インストルメンテーション コードをソース コードに密接にリンクするために、さまざまなプロバイダーに適応し、環境変数を Canyon サーバーに送信してレポート ID を交換しました。これにより、カバレッジ データの集約と計算が完了した後、カバレッジ ソース ファイルの関連付けと表示が容易になります。

また、さまざまなパイプライン (AWS、GitLab CI) 内の環境変数 (branch、sha) を読み取り、後続のカバレッジデータを対応する GitLab ソースコードに関連付けることができる Babel プラグイン (babel-plugin-canyon) も提供しています。

babel.config.js

 module.exports = { plugins: [ [ 'babel-plugin-canyon', { provider: 'gitlab', branch: process.env.CI_COMMIT_REF_NAME, sha: process.env.CI_COMMIT_SHA, }, ], ], };

サポートされているプロバイダー:

  • Azure パイプライン
  • サークルCI
  • ドローン
  • Githubアクション
  • GitLab CI
  • ジェンキンス
  • トラビス CI

コードプローブをインストルメントすると、ビルド成果物のコンテキストにコードプローブが追加され、コード成果物全体が30%増加することに注意してください。これを本番環境にデプロイすることは推奨されません。

V. テストと報告

インストルメンテーションが完了し、テスト環境にデプロイされたら、テストを開始できます。PlayWrightを例に挙げると、フロントエンドアプリケーションサイトでインストルメンテーションが正常に完了すると、`window` オブジェクトに `__coverage__` オブジェクトと `__canyon__` オブジェクトがマウントされます。PlayWrightのテストプロセス中に、これらのデータを収集し、Canyonサーバーに報告する必要があります。

劇作家の例:

 const {chromium} = require('playwright'); const main = async () => { const browser = await chromium.launch() const page = await browser.newPage(); // 进入被测页面await page.goto('http://test.com') // 执行测试用例// 用例1 await page.click('button') // 用例2 await page.fill('input', 'test') // 用例3 await page.click('text=submit') const coverage = await page.evaluate(`window.__coverage__`) // 收集上报覆盖率upload(coverage) browser.close() } main()

Ctripは独自のUI自動化プラットフォームであるFlybirdsを所有しており、Canyonのカバレッジデータ収集とレポート機能をFlybirdsに統合しました。実際のブラウザUI自動化テストにおけるカバレッジ収集シナリオは非常に複雑であり、これは主に複数ページ(MPA)にわたるカバレッジデータをいつ収集するかが不明確であるためです。

シングルページアプリケーション(SPA)とマルチページアプリケーション(MPA)

テストケース実行後、シングルページアプリケーション(SPA)とマルチページアプリケーションの両方において、レポートステップでは、ページの `window` オブジェクトから `__coverage__` オブジェクトをCanyonサーバーにレポートします。SPAの場合、すべてのテストコンテンツがSPA内に存在し、カバレッジデータは `window` オブジェクトに保持されるため、このステップは比較的簡単です。しかし、マルチページアプリケーションでは、ルート変更によって `window` オブジェクトが再作成され、結果として `coverage__` オブジェクトが失われます。そのため、このタイミングは非常に重要です。広範な実践的な検証を通じて、ブラウザの `onvisiblechange` メソッドを発見しました。

  • 可視性の変化

ブラウザの可視性が変更されると、カバレッジデータが報告されます。`visibilitychange` の場合、データが重複して報告される可能性があることに注意してください。ただし、カバレッジ統計の場合、実行されなかった複数のコードインスタンスをマージしても、特定のカバレッジメトリックには影響しません。

  • 後で取得

Chrome は革新的な JavaScript API、fetchLater() を積極的に導入しています。この新しい API は、ページが閉じられた後のデータ送信プロセスを完全に簡素化し、ページが閉じられた後やユーザーが Chrome を離れた後でも、将来のある時点でリクエストを安全かつ確実に送信できるようにすることを目的としています。

この API のリリースは、ブラウザが閉じられたときにデータを収集するだけで、困難なマルチページ アプリケーション (MPA) データ収集の問題を効果的に解決するため、非常に喜ばしいものです。

注: fetchLater() は、Chrome バージョン 121 (2024 年 1 月リリース) 以降、実際のユーザーによるテストに使用でき、Chrome 126 (2024 年 7 月) まで継続されます。

VI. 集約

カバレッジデータは同一バージョンのコードから取得されるため、集計が可能です。Canyonは内部的にreportIDを使用してテストケースを関連付け、集計ディメンションを絞り込みます。これにより、膨大なカバレッジデータを有限の数、つまりケース数に集約することが可能になります。

 /** * 合并两个相同文件的文件覆盖对象实例,确保执行计数正确。 * * @method mergeFileCoverage * @static * @param {Object} first 给定文件的第一个文件覆盖对象* @param {Object} second 相同文件的第二个文件覆盖对象* @return {Object} 合并后的结果对象。请注意,输入对象不会被修改。 */ function mergeFileCoverage(first, second) { const ret = JSON.parse(JSON.stringify(first)); delete ret.l; // 移除派生信息Object.keys(second.s).forEach(function (k) { ret.s[k] += second.s[k]; }); Object.keys(second.f).forEach(function (k) { ret.f[k] += second.f[k]; }); Object.keys(second.b).forEach(function (k) { const retArray = ret.b[k]; const secondArray = second.b[k]; for (let i = 0; i < retArray.length; i += 1) { retArray[i] += secondArray[i]; } }); return ret; }

エンドツーエンドのテストカバレッジデータの特徴の一つは、個々のデータ量が非常に多いことです。これは、プロジェクト全体をインストルメントすると、ソースコード全体の30%に相当します。Trip.comのフライトサイトの予約ページにおける自動UIテストケースは、1セッションあたり最大2,000回のレポートを生成し、それぞれ10MBのデータを含んでいます。このデータ量は、Canyonサーバーにとって大きな負担となります。

大規模な単一データポイントと高頻度のデータレポートシナリオでは、リアルタイムのデータ集約と計算を実現することは困難です。Canyonはメッセージキューを使用してデータを消費し、ステートレスなサービスとして設計されているため、クラウドネイティブ時代のコンテナ化されたデプロイメントに適しています。HPAのエラスティックスケーリングを使用することで、さまざまなシナリオでテストカバレッジレポートを適用できます。

VII. 報告

カバレッジレポートの表示には、istanbul-report のインターフェーススタイルを採用しました。しかし、istanbul-report は静的な HTML ファイルの生成のみを提供するため、現代のフロントエンドデータ生成 HTML モードには適していません。そのため、ソースコードを参照し、monaco-editor を使用してソースコードカバレッジをマークしました。

 const decorations = useMemo(() => { if (data) { const annotateFunctionsList = annotateFunctions(data.coverage, data.sourcecode); const annotateStatementsList = annotateStatements(data.coverage); return [...annotateStatementsList, ...annotateFunctionsList].map((i) => { return { inlineClassName: 'content-class-found', startLine: i.startLine, startCol: i.startCol, endLine: i.endLine, endCol: i.endCol, }; }); } else { return []; } }, [data]);

着色後の効果:

8. コードカバレッジの変更

変更コード カバレッジの場合、計算式は「カバーされる新しく追加されたコード行数 / 新しく追加されたコード行の総数」となります。

compareTarget を設定することで比較対象を指定し、GitLab の git diff インターフェースからのカバレッジ データと組み合わせて変更されたコード行を取得します。

 /** * returns computed line coverage from statement coverage. * This is a map of hits keyed by line number in the source. */ function getLineCoverage(statementMap:{ [key: string]: Range },s:{ [key: string]: number }) { const statements = s; const lineMap = Object.create(null); Object.entries(statements).forEach(([st, count]) => { if (!statementMap[st]) { return; } const { line } = statementMap[st].start; const prevVal = lineMap[line]; if (prevVal === undefined || prevVal < count) { lineMap[line] = count; } }); return lineMap; }

IX. React Nativeカバレッジ収集スキーム

Ctripのモバイルテクノロジースタックは主にReact Nativeですが、Babelコンパイルをベースとしているため、当社のインストルメンテーションソリューションにも同様に応用できます。さらに、社内で統一されたReact Nativeプロジェクト構造のおかげで、コンパイル時のインストルメンテーションをパイプラインに統合できました。パイプライン内では、「通常パッケージ」と「インストルメンテーションパッケージ」の両方をパッケージ化しており、UI自動化と組み合わせることで、記録、再生、カバレッジメトリクスを収集するための完全なテストシステムを構築できます。

WebSocket を使用してシミュレータ内でカバレッジデータを公開します。

 // 创建WebSocket连接const socket = new WebSocket('ws://localhost:8080'); // 当WebSocket连接打开时触发socket.onopen = () => { console.log('Connected to coverage WebSocket server'); }; // 当收到WebSocket消息时触发socket.onmessage = event => { try { if (JSON.parse(event.data).type === 'getcoverage') { // 发送覆盖率数据socket.send(JSON.stringify(payload)); } } catch (e) { console.log(e); } }; // 当WebSocket连接关闭时触发socket.onclose = () => { console.log('Disconnected from coverage WebSocket server'); };

現在、Ctripの航空券予約アプリのすべてのモジュールがCanyonに統合されています。istanbuljsは実践を通して、Canyonのカバレッジデータを効果的にインストルメント化し、収集できるようになりました。テストチームは、各本番リリースの前に、Canyonのカバレッジデータメトリクスを使用してリリースの品質を測定しています。

10. カバレッジ改善優先リスト

Canyonシステムを初めて導入する際、ユーザーは課題に直面します。UI自動テストケースが十分に用意されていないと、大規模アプリケーションのコードカバレッジが著しく低下してしまう可能性があるのです。当初は、Istanbulのコードカバレッジレポートを提供するだけでは、チームにカバレッジ改善方法を効果的に指導することができず、全員が混乱し、途方に暮れてしまうことになります。

この課題に対処するため、徹底的な調査を実施した結果、この企業では既に本番環境向けに成熟したコードカバレッジ収集システムが導入されていることがわかりました。この結果に基づき、このシステムのデータと弊社独自のカバレッジデータを組み合わせて「カバレッジ改善優先度リスト」を作成することにしました。このリストの目的は、開発チームに明確な指針を提供し、コードカバレッジの改善を優先すべき領域を把握できるようにすることです。

このガイドラインをより科学的かつ実用的なものにするために、私たちはカバレッジ重み付けの式を開発しました。

本番環境カバレッジ × 100 × 0.3 + (1 - テストカバレッジ) × 100 × 0.3 + 関数数 × 0.2

この計算式により、本番環境での使用率が高く、行数が多く、テストカバレッジが低いコードファイルを優先的に特定し、開発チームに的を絞った改善提案を提供できます。このアプローチは、コード品質の向上だけでなく、全体的なカバレッジの管理強化にもつながります。

XI. コミュニティの推進

この記事の公開をもって、Canyonを正式にオープンソース化します。JavaScriptは現在最も人気のあるプログラミング言語ですが、エンドツーエンドのテストカバレッジ収集の分野は不足しています。私たちのコード開発は、istanbuljsやMonaco editorといった優れたオープンソースプロジェクトに基づいています。Canyonをオープンソースプロジェクトとして立ち上げることで、コミュニティの支持を得て、多くのJavaScript開発者の参加を得られると確信しています。

Canyonには、今後の開発の余地が大きく残されています。例えば、本番環境のインストルメンテーション収集機能はまだ検証されておらず、Playwright、Puppeteer、Cypressといった自動テストツールとの緊密な連携も不足しています。これらの点については、既に今後の開発計画に盛り込まれています。Canyonの今後の開発において、Ctripとコミュニティの皆様からより多くのご参加をいただけることを期待しています。