HUOXIU

データ サンドボックスと LLM テスト ケースの自己修復に基づく UI 自動テスト プラットフォーム

出典: Bilibili Technology


プロジェクト参加者:

Gu Yifan、Chen Yuguang、Zhang Youzhong、Yang Yuhao、Fan Zhizheng、Xiong Mengyuan、He Xuan、Tan Nan


UI自動テストは製品の品​​質をある程度保証することができ、コスト削減と効率性向上の観点からその重要性がますます高まっています。理想的には、UI自動テストは多くのオンライン問題を回避できるだけでなく、製品のリリースを加速させるのに役立ちます。しかし、現実はそう簡単ではありません。多くの場合、テストスクリプトの作成には膨大な作業量が必要であり、アプリケーションの頻繁な反復により、これらのスクリプトはすぐに陳腐化します。さらに、ネットワークデータの変動性はテスト結果を不安定にし、テストの信頼性に影響を与えることがよくあります

これらの課題に対処するため、UI自動テストの導入障壁を下げ、使いやすさを向上させるUI自動テストプラットフォーム「AutoMotion」を開発しました。このプラットフォームは、テストケースを容易に生成できるだけでなく、最新の大規模言語モデルを活用した自己修復機能を備えており、インターフェースの合理的な変更にインテリジェントに適応し、テストスクリプトを自動的に修正します。さらに、テストデータを分離・制御するデータサンドボックスを構築することで、テストの一貫性と再現性を確保します。

この記事では、AutoMotion プラットフォームの設計理念、コア機能、実装原則を紹介し、皆様とアイデアを交換しながら共に進歩していくことを願っています。


背景


私たちのチームは、UI 自動化テストに Cypress などの一般的なツールを使用しようとしましたが、主に次の 4 つのタイプに分類できる多くの問題と問題点に遭遇しました。


手書きのスクリプトはコストがかかる


ページ数が多い場合、自動テストスクリプトを手動で作成するとコストが非常に高くなります。(この点についてはこれ以上の説明は不要です。)

(一部のツールでは記録されたアクションに基づいてスクリプトを生成できますが、その機能は一般的に制限されています。たとえば、Cypress が提供するツールではスクロールアクションを記録できません。)


ネットワーク データが変更されると、テスト結果が信頼できなくなる可能性があります。


オンライン環境、プレプロダクション環境、テスト環境など、データは常に変化するため、実行の成功または失敗が実際の機能ロジックに問題があるかどうかを正確に反映していないケースが多くあります。これは、特定のテストアカウントが特定のテストケースを実行するために割り当てられた場合でも当てはまります。例えば、べき等性のない操作は問題を引き起こす可能性があります。

簡単な例を挙げましょう。イベントページで、ユーザーはイベントへの参加登録ができます(1人1回のみ登録できます)。テストケースは「アカウント1にログインし、イベント登録ボタンをクリックすると、ページに「登録成功」と表示される」というものです。テストケースの初回実行で成功すると、イベントのステータスが「登録済み」に変わります。2回目の実行では登録は失敗しますが、フロントエンドとバックエンドのコードは正常に動作します。

もちろん、データをリセットするスクリプトを作成することも考えられます。しかし、まず、バックエンド開発者の協力が必要です。次に、データの基盤となるロジックが非常に複雑になる可能性があります(例えば、上記の例では、アカウント1の登録ステータスをリセットできますが、イベントの登録期限が迫っていたり、イベントが中止になったり、登録枠が上限に達したりする可能性があります)。さらに、プロジェクトやシナリオごとに異なるデータを用意する必要があり、実装コストが非常に高くなります。さらに、オンラインデータのリセットは困難です。


プロジェクトの反復が頻繁に行われると、テスト ケース スクリプトのメンテナンス コストが高くなります。


ページが反復処理されると、その反復処理が特定のユースケースとは機能的に無関係であっても、そのユースケースのスクリプトは効果がなくなることが多く、更新が必要になります。

参考までに次の例を参照してください。

テストケースは、「...タスクの「完了へ進む」ボタンをクリックします...」です。

ページ構造は図のようになります。



自動テストスクリプトでは、「.task > .btn」セレクタを使って「Go to Complete」ボタンを選択し、クリック操作を実行できます。Cypressコードは以下のとおりです。



 cy.get('.task > .btn').click() 


ただし、後続の反復では、「.btn」の外側に「.task-content」の追加レイヤーを追加するなど、いくつかの変更が発生する可能性があります。



この時点では、「.task > .btn」経由では取得できなくなっているため、テスト ケース スクリプトでセレクターを「.task > .task-content > .btn」に更新する必要があります。

もちろん、この特定の例では、セレクタを「.task .btn」と記述することもできます。これにより、「.btn」の外側に任意の数の要素をラップしても影響はありません。しかし、「.btn」を「.btn-primary」に変更した場合はどうなるでしょうか?同様の状況は数多くあります。古いテストケースとは無関係であるはずの多くのイテレーションで、依然としてテストケーススクリプトの更新が必要となり、メンテナンスコストが大幅に増加します。場合によっては、各イテレーションで大量のテストケーススクリプトの更新が必要になることもあり、自動テストが完全に無意味になり、手動の回帰テストの方が効率的になります。


標準プロセスに統合する方法


さまざまなプロジェクトの自動テストタスクを、開発およびリリースワークフローに簡単かつ柔軟に統合する方法。(この点についてはこれ以上の説明は不要です。)

上記の問題を解決するために、私たちは独自の UI 自動テスト プラットフォームを開発することにしました。


AutoMotion - UI自動化テストプラットフォーム


上記の 4 つの主要な問題に対処するために、プラットフォームは次の 4 つの解決メカニズムを提供します。



上記の 4 つの主要な問題に対処するために、プラットフォームは次の 4 つの解決メカニズムを提供します。


テストケーススクリプトの記録と生成


これは手書きのスクリプトの高コストの問題を解決するために使用されます。

このChrome拡張機能を使用すると、記録を有効にすると、ウェブページ上のユーザーアクションをシミュレートできます。この拡張機能はユーザーのアクションを認識し、UI自動化テストスクリプトを自動生成します。クリック、キーボード入力、スクロール、ドラッグなど、ほとんどの一般的な操作をサポートしています。



データサンドボックス


これは、ネットワーク データの変更によるテスト結果の信頼できない問題に対処するために使用されます。

これをより正確に理解するために、まず問題そのものからより一般的な観点に焦点を移してみましょう。

ブラウザは、フロントエンドコード、ユーザーアクション、インターフェースデータ、日付、乱数などを「入力」とし、レンダリングされたインターフェースを「出力」とする関数と考えることができます。ほぼ99.9%のケースにおいて、この関数は決定論的で、副作用がなく、予測可能であると考えられます(つまり、上記の「入力」が変化しなければ、ブラウザを何度実行しても「出力」は変化しないはずです)。

それでは、上記の観点から UI 自動テストを再検討してみましょう。

下図に示すように、あるイテレーション後にUI自動テストケースを実行すると、「ユーザーアクション」は前回実行時(テストスクリプトにハードコードされている)と比較して変化がありません。しかし、複数の入力項目に変化が生じる可能性があります。レンダリング結果が変化し、テストケースが失敗(中断またはアサーション不一致)した場合、「フロントエンドコードの変更」に直接起因するとは考えにくいです。



UI 自動テスト ケースが実行されるたびにフロントエンド コードのみが変更されると、テスト ケースが失敗した場合に、その失敗の原因が「フロントエンド コードの変更」であるとより正確に判断できます。



こうして、「データ サンドボックス」というアイデアが生まれました。

テストケース記録フェーズでは、ユーザー操作を記録しながら、インターフェースリクエスト、ストレージ、Cookie、ウィンドウサイズなどの外部データが収集され(このデータを自分で変更することもできます)、テストケース実行フェーズで 1:1 で「再現」されます。

もちろん、「データサンドボックス」の使用にはトレードオフが伴います。APIデータからの干渉を回避し、多くの手間を省く一方で、実質的にはフロントエンドコードのみのテストとなります。そのため、プラットフォームでは必要に応じて特定のデータサンドボックスを無効にすることも可能です(例えば、バックエンドロジックをテストするためにAPIリクエストサンドボックスを無効にしたり、ログインの問題を回避するためにCookieサンドボックスを維持したりするなど)。


テストケースの視覚的な編集とテストケースの自己修復メカニズム


これは、頻繁なプロジェクトの反復によりテスト ケース スクリプトのメンテナンス コストが高くなるという問題に対処するために使用されます。

まず、上で紹介した「テスト ケース スクリプトの記録と生成」機能により、この問題はほぼ解決されました。テスト ケースは、更新が必要なときに簡単に再記録できます。

第二に、このプラットフォームは以下も提供します。

  1. 「ユースケースのビジュアル編集」機能により、小さな更新のみが必要な場合にユースケースをすばやく編集および更新できます。

  2. 「テスト ケース スクリプト編集」機能を使用すると、記録されたテスト ケースを Cypress スクリプトに直接変換して (Playwright サポートは後で検討)、より柔軟で制限のない編集を行うことができます。

これらの更新方法はすべて非常に便利ですが、実際の使用では依然として大きな問題が存在します。

数回のイテレーションを経て、多くのテストケースが実行に失敗することがあります。しかし、これらの失敗のほとんどは、新たに発生したバグではなく、ページ構造の合理的な更新が原因です。このような場合、エラーメッセージを一つ一つ手動で確認し、それが機能の問題なのかバグなのかを確認する必要があります。さらに、機能の問題が原因で失敗したテストケースを速やかに更新しないと、次回の実行時に再び失敗する可能性が高くなります。この悪循環が続き、失敗するテストケースの数が増えていきます。そのため、イテレーションのたびにトラブルシューティングとテストケースの更新に多大な労力を費やす必要があり、「自動テスト」の本来の目的とは全く矛盾しています。

実際、こうした問題を観察してみると、そのほとんどが同じ「単純な」理由から生じていることがわかります。ページが更新された後、テストケースの実行時にターゲット要素を取得できないのです(前述の「③ 頻繁なプロジェクトの反復により、テストケーススクリプトのメンテナンスコストが高くなる」の例を思い出してください)。

この問題に対処するため、従来のCSSセレクタルールを拡張し、既存のLLMに基づいたインテリジェントなターゲット要素認識と「テストケースの自己修復」を実装しました。これにより、ページ構造の変更後(主要な機能更新を除く)でもターゲット要素を識別し、最新のページ構造に合わせてテストケーススクリプトを自動的に更新できるようになります。


オープンAPI


これは、標準プロセスにどのように統合するかという問題を解決するために使用されます。

記録・保存されたテストケースごとに、プラットフォームは対応するオープンAPIを生成します。このAPIを呼び出すと、テストケースが実行され、結果が取得されます。このAPIは、GitLab、パイプライン/ビルド/リリースプラットフォーム、CLIツールなどと柔軟に統合でき、あらゆるプロセスノードでの自動テストを実現します。

このプラットフォームでは、定期的な自動テストのためにタイマーを設定することもできます。


制度と原則の紹介


次のセクションでは、プラットフォームの主要なテクノロジーとその基礎となる考慮事項の一部を紹介します。


テストケースの入力


Chrome拡張機能



テストケースの入力はChrome拡張機能によって実装されています。記録が開始されると、拡張機能は以下の処理を実行します。

  • ページのヘッダーに JavaScript スクリプトが埋め込まれており、ページの実行時環境に侵入し、ページのウィンドウ オブジェクトを操作して記録プロセス中にすべての操作とストレージ データをキャプチャします。

  • devtools ページで記録中にすべてのインターフェース データをキャプチャします。

  • バックグラウンド ページ (サービス ワーカー) で Cookie をキャプチャします。

  • コンテンツ スクリプトで、ユーザー エージェントやウィンドウ サイズなどの情報を取得します。

記録後、プラグインは収集した情報を定義済みの仕様に準拠したテストケースDSLに変換し、バックグラウンドで保存します。上記の基本機能に加えて、プラグインはDSLの可視化/編集、要素の可視化と選択、環境認識、異常検知、テストケースの試行実行などの機能も実装しています。

Chrome拡張機能の制限により、上記の機能を実装するには、Chrome拡張機能がサポートする機能に応じてモジュールを分割する必要があります。さらに、多くのモジュールは直接通信できないため、データフローの管理が比較的困難です(そのため、通信データのタイプとフォーマットを標準化していますが、ここでは詳細は省略します)。以下に詳細を示します(図は参考用であり、概要を示すだけで十分です)。



ビルドとアップデート


このChrome拡張機能の構造は通常のフロントエンドプロジェクトとは異なり、現在は社内でのみ使用されているため(Chromeウェブストアにはまだアップロードされていません)、ビルド、パッケージング、バージョンアップデートにいくつかの変更が加えられています。これらの変更はビルドスクリプトにカプセル化されており、実行時に以下のプロセスを自動的に完了できます。

  1. webpack を使用して devtools ページ (つまり、このプラグイン内のすべての UI 要素) を構築します。

  2. devtools ページにあるファイルを除くすべてのファイルをビルドします (例: 環境変数を均一に置き換えます)。

  3. マニフェスト内のバージョン番号を更新します。

  4. プロジェクト全体を zip ファイルに圧縮し、オンラインにアップロードします。

  5. データベースを最新のバージョン番号に更新します。

  6. (Chrome 拡張機能の古いバージョンを開くと、更新を求めるメッセージが表示されます。クリックしてダウンロードするだけです)。


テストケースの実行



テストケースの実行はNode.jsとCypressを用いて実装しています。Node.jsサービスはコンテナ内のCypressプロジェクト環境を初期化します。テストケースの実行が必要になると、Node.jsサービスはテストケースDSLを実行可能なCypressスクリプトと関連データファイルに変換します。これらのデータファイルには、ユーザーアクションやデータサンドボックスなどの情報が含まれます。その後、スクリプトをCypress(複数の同時Cypressインスタンスをサポート)で実行し、最終的に実行結果を生成してフォアグラウンドトリガーまたはアラートに送信します。


行動監視



前述の通り、テストケースの記録中に、Chrome拡張機能はページのヘッダーにJavaScriptスクリプトを挿入し、オペレーターの行動をリッスンします。ここでは、そのリッスン方法についてさらに詳しく説明します。


基本的なイベントリスナー


Chrome拡張機能に埋め込まれたJavaScriptは、ページのウィンドウオブジェクトにグローバルイベントを委譲し、ユーザーアクションをリッスンします。例:


 window.addEventListener('input', this.handleInput, true) 


特別イベントリスナー


上記の「基本的なイベント リスナー」メソッドは十分なようですが、詳しく調べてみると、mouseenter、mouseleave、mousemove、mouseover、mouseout などのイベントも、この方法でリッスンするためにウィンドウまたはドキュメント ノードにバインドできるでしょうか?

これは明らかに不合理です。例えば、`document` で `mouseenter` イベントをリッスンすると、マウスがページ上の任意の要素に移動するたびにイベントがトリガーされますが、これは冗長です。これを `click` と比較してみましょう。ページ上のユーザークリックのほとんどは「意味のある」( 「ページ機能」全体への意味のある入力)ため、ページ上のどこでトリガーされた `click` イベントも記録できますが、マウスの動きのほとんどはページにとって実質的な意味を持ちません。

ここで、逆の考え方をする必要があります。つまり、これらのイベントが既にバインドされている要素のみが、意味のあるイベントをトリガーする可能性が高く、これらの要素のみをリッスンすればよいということです。例えば、ページ上に要素Aがあり、マウスオーバー時にヒントを表示するための mouseenter イベントがバインドされている場合、テストケースの記録中にユーザーが要素A上にマウスを移動してヒントの表示をトリガーした場合、mouseenter イベントを記録し、テストケースの実行中にトリガーする必要があります。それ以外の場合は、記録する必要はありません。では、これらのイベントがバインドされている要素をどのように把握するのでしょうか?ページ内の window.Element.prototype.addEventListener 関数を、独自に構築した関数に置き換えることができます。この関数は、任意のページ内の元の「イベントバインディング」動作をすべてインターセプトできます。もちろん、元の関数は、元の機能に影響を与えることなく呼び出す必要があります。コードを以下に示します。















 const originalAddEventListener = window.Element.prototype.addEventListener; // すべての要素の addEventListener を置き換えます window.Element.prototype.addEventListener = function(type, originalListener, ...others) { // すべての要素のリスナー コールバック関数を置き換えて、mouseenter、mouseeleave、mouseover、mouseout イベントをインターセプトします const listener = function(event) { if (isRecording && ['mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousemove'].includes(event.type) && event.target === this) { handleMouseAction(event) } originalListener.call(this, event) } return originalAddEventListener.call(this, type, listener, ...others)} 


ターゲット要素の位置


「動作」は実際には「アクション」と「ターゲット要素」という2つの要素から構成されています。例えば、「ユーザーが要素Aをクリックする」という場合、「アクション」は「クリック」で、「ターゲット要素」は「要素A」です。「アクション」を特定する方法は既に説明しましたので、ここでは「ターゲット要素」を特定する方法を紹介します。


基本的なポジショニング


配置方法は、ルートノードからターゲット要素までの一意のパスを表すCSSセレクタを使用します。例:body > div > #app > div.app-right > div > div:nth-child(2) > div.text


基本的な位置決めの強化


まずは下図の例をご覧ください。「エクスポート」ボタンが対象要素で、「基本配置」で記録されたセレクターは「xxx > div:nth-child(2)」のようになります。

ただし、後のバージョンでは、「エクスポート」の前に「リセット」ボタンが追加されました。

この時点で、「xxx > div:nth-child(2)」に「リセット」ボタンが見つかります。

ここで、通常のCSSセレクタの記述力では不十分であることがわかります。ある要素について、そのクラスを正確に記述できるだけでなく、現在のレイヤー内のどの要素であるかも正確に記述できます。しかし、この2つを組み合わせることはできません。つまり、「現在のレイヤー内のどの要素がxxクラスであるか」を記述することはできません(フロントエンドの世界における「不確定性原理」でしょうか?😑)。

したがって、CSSセレクタ拡張に基づいた新しいルールセットを定義する方が良いでしょう。例えば、「 xx > div.btn.primary:nth(2) 」のように、これは「xxの子ノードのうち、div.btn.primaryに準拠するすべてのノードを選択し、その中から2番目のノードを選択する」という意味です。上記の例では、「リセット」ボタンを追加しても、「エクスポート」ボタンの位置は影響を受けません。

さらに、「xx > div.btn.primary:contains('Export'):nth(1)」のようなテキストコンテンツ情報を追加することで、新しく追加された「リセット」ボタンが「.btn.primary」であっても、何の影響も与えないようにすることができます(もちろん、情報が多すぎると、ページの反復処理後に正確なマッチングを行う際にミスが発生しやすくなります。ただし、「あいまいマッチング」の場合は、情報が多いほど良いのですが、これについては後述します)。

(実際には、XPath でこの情報を記述できますが、最新の Cypress では XPath がサポートされなくなり、XPath 検索を手動で実装するのはコストがかかるため、「CSS セレクター」のカスタム拡張バージョンを手動で実装することを選択しました。)


LLMに基づくターゲット要素のインテリジェントな識別とユースケースの自己修復


プラン


拡張 CSS セレクタを使用すると、ページ構造の変更後に要素の配置が失敗する可能性が軽減される場合がありますが、要素クラス、要素の位置、要素のコンテンツ テキストの変更など、失敗の原因となる状況は依然として多くあります。

人間による回帰テストでは、私たち「賢い人間」はほとんどの場合、変更されたターゲット要素を簡単に特定できます。では、UI自動化テストにおいて、人間に近いターゲット要素認識能力を実現することは可能でしょうか?

最も明白な解決策は、画像認識に基づいて要素を選択することです。これはほぼ唯一の最適な選択肢でしたが、近年のLLMの急速な発展に伴い、LLMに基づく対象要素のインテリジェントな認識という別の可能性を検討しました。最新のLLMモデルのテキスト理解能力は非常に優れており、「ページ構造」自体はテキスト(DOM)によって記述されています。画像認識の経験が限られていること、そして2番目の解決策は「対象要素のインテリジェントな認識」以外にも様々なシナリオに適用できると期待されることから、 2番目のアプローチを選択しました。

解決策はおおよそ次のようになります。テストケース実行中にセレクタの取得に失敗した場合、実行は一時停止され、ページDOM、セレクタ、その他の情報がLLMに渡されて処理されます。LLMは「古い」セレクタが実際に取得しようとしている要素を識別し、更新されたセレクタパスを返し、対応するテストケースを更新して再実行することで、「テストケースの自己修復」を実現します。


DOM圧縮


LLMに情報を入力する前に、重大な問題が発生します。ページのDOMが非常に大きくなると、LLMの処理時間が非常に長くなり、LLMがサポートするサイズ制限を超える可能性があります。例えば、Bilibiliのホームページをレンダリングした後のDOMサイズは次のとおりです。



なんと22万個のトークンがあります!しかし、この情報のほとんどは「対象要素の識別」にはほとんど意味がないので、すべて削除して「DOM圧縮」を実行できます。

  • スクリプト、スタイル、リンクなどの構造的に無関係なタグを削除します。

  • スペースと改行を結合する

  • 残りのすべてのタグについては、タグ、ID、クラス、テキスト情報のみを保持し、その他すべてを削除します。

これらの手順を実行すると、ページで使用されるトークンの数は 13,000 に大幅に削減されました。



しかし、残りのDOM要素には依然として多くの重複コンテンツが存在します。例えば、特定のクラス名がページ上で何度も出現し、多くの文字数を占める場合があります。そこで、「クラス圧縮マッピングテーブル」を生成し、各クラス名を数値にマッピングすることができます。最終的には、すべてのクラス名を0から500までの数値で表すことができます。もちろん、各桁の「密度」はまだ低すぎます(10種類の値しか含めることができません)。また、クラス名は数字で始めることができないため、文字を使って「カウント」する方が適切です。例えば、aからzで始まり、1を足してaaにするなどです。ほとんどのクラス名は2文字以内に圧縮されます(テストの結果、この手順だけでページ上のトークン数をさらに9,000まで削減できます)。

同様に、タグ名も圧縮できます。「class」フィールド自体も圧縮可能です(例えば「c」に圧縮)。

これらの圧縮後、DOMは確かに縮小し続けます。しかし、これらの圧縮によって、クラス名(例:"task-btn")やタグ名といった多くの意味情報が失われます。これらの情報は、LLMがページコンテンツを理解し、対象要素をより正確に識別するのに役立ちます。そのため、最終的にはこれらの更なる圧縮方法は選択しませんでした(別の方法として、LLMに圧縮マッピングテーブルも通知する方法もありますが、LLMの理解が難しくなるため、テストが必要です)。

実際、ビリビリのホームページは、ビリビリのDOMの中で最も大きなページの一つです。最初の圧縮ステップを踏むと、ほとんどのページのトークン数は13,000をはるかに下回ります(例えば、多くのモバイルページは圧縮後、1,500~5,000トークンになります)。


プロンプト


DOMを圧縮したら、プロンプトを構築できます。プロンプトの構造はおおよそ次のようになります。



このプロセスは、まず LLM に例を通して「あいまいマッチング」(つまり、「不正確なセレクター」に基づいて正しい要素を見つける)を実行する方法を教え、次に圧縮された DOM とセレクターが LLM に渡されてタスクを完了します。

実際の効果を評価するために、いくつかの予備テストを実施しました。



通常の状況では、「ユースケースの自己修復」機能は十分です(現在最適化中です)。状況が大幅に変更され、「自己修復」が困難な場合は、ユーザーにユースケースの再記録を促すメッセージが表示されます。


実行プロセス


マーカー要素のインテリジェントな識別とユースケースの自己修復を採用した後の実行フローは次のとおりです。



微調整?


現在、LLMの要件はすべてプロンプトに含まれていますが、これは必ずしも最善のアプローチとは言えません。プロンプトの長さには制限があり、LLMに十分な情報を提供して精度を向上させることは不可能であり、処理時間とコストも大幅に増加します。

ファインチューニングにより、特定のニーズやデータセットに基づいてモデルをカスタマイズ・最適化し、特定のシナリオに適応させることができます。前述の純粋なプロンプトアプローチと比較して、大規模なモデルにはるかに多くの「あいまい一致」例を事前に入力できるため、ターゲット要素の識別能力がさらに向上し、プロンプトのサイズも大幅に削減できます(プロンプトの「事前学習」部分を削除できます)。したがって、ファインチューニングはより優れたアプローチであると考えられます。時間の制約のため、まだ試していませんが、後ほどさらに詳しく検討します。


一意の識別子の場所


基本的な解決策


上記は、従来のポジショニングとインテリジェントなポジショニングを組み合わせたソリューションであり、ほとんどのシナリオを比較的スムーズにカバーできます。ただし、LLM(ローカルリンクモデル)エラーが発生する可能性があり、一部の機密ページはLLMの受け渡しに適さない場合があります。統合コストは若干高くなりますが、安定性の高い他の代替手段はありますか?もちろん、すべてのターゲット要素に一意の識別子を追加する方法があります。

テスト ケースを記録する前に、操作を記録する必要がある要素に次のような一意の識別子を割り当てることができます。

<div data-atm-id="(uuid)" (元のプロジェクトのビジネスロジックに影響を与えないようにするため、ここでは「id」属性は直接使用されていません)。テストケースを記録する際、対象要素にこの識別子がある場合は、その識別子を記録します。この識別子はグローバルに一意であるため、DOM構造を変更しても要素の取得には影響しません(ただし、要素が削除された場合は、テストケースが新しい要件を満たさなくなり、再記録する必要があることを示します)。


問題点と改善点


対処する必要がある詳細だが重要な問題がいくつかあります。

質問1:



要素 2 のみが一意の識別子にバインドされているが、記録中に要素 1 がクリックされた場合、不一致が発生し、記録された要素は引き続き要素 1 のセレクターになります。

解決策: テスト ケースを記録するときに、キャプチャされた要素に data-atm-id がない場合、data-atm-id を持つ最も近いノードを見つけて、それを実際のターゲット要素として記録します。

質問 2: 一部の npm コンポーネント ライブラリでは、コンポーネントの内部要素にタグを付けるのが難しいです。

解決策:「data-atm-parentId」識別子を追加し、コンポーネントの最外層に記述します(記述が不便な場合は、コンポーネントの外側にdiv要素を追加して識別子を記述することもできます)。テストケースを記録する際に、キャプチャした要素にdata-atm-idがない場合(問題1の解決策が満たされていない場合)、上位にdata-atm-parentIdを持つ最も近いノードを見つけ、それを先頭とするセレクターを構築します(例:「[data-atm-parentId="xxx"] > div」)。見つからない場合は、現在の要素の通常のセレクターを記録します。


トレード・オフ


もちろん、一意の識別子の場所にはいくつかの欠点もあります。

  1. テスト対象のプロジェクトに侵入してプロジェクト コードを変更する必要があるため、一定の統合コストが発生します。

  2. コンポーネント ライブラリ内の要素を処理する方法はまだ十分に安定しておらず、信頼性も十分ではありません。

  3. 完全に自動化されたテスト ケース生成 (最後の「展望」セクションで説明します) を実現すること、つまり数百または数千ページのテスト ケースを生成することが目標である場合、このソリューションは適していません。

「要素ローカリゼーション」において、シンプルかつ効率的で、かつ安定性と信頼性も兼ね備えた完璧なソリューションはほぼ存在しないことは明らかです。現在私たちができることは、上記のソリューションを組み合わせ、ユーザーがニーズに合わせて選択できるようにすることです。しかしながら、私たちの主な焦点でもある「インテリジェントなターゲット要素の特定とユースケースの自己修復」については、継続的な最適化の余地がまだあると考えています。さらに、微調整やより強力なLLMの登場により、このソリューションはさらに強化される可能性があります。


データサンドボックス


ここでは、「データ サンドボックス」のコア コンポーネントである「ネットワーク リクエスト サンドボックス」を紹介します。


基本的な解決策


基本的な解決策は非常にシンプルです。

  1. テスト ケースの記録中、すべての API データは Chrome プラグインの chrome.devtools メソッドを使用してキャプチャされます。

  2. テストケース実行時に、記録時に記録された該当ネットワークデータとインターフェース入力パラメータ(url(クエリを含む)+メソッド+本体)を照合し、Cypressのcy.interceptメソッドを使用してインターセプションとモックを実行します。


問題点と改善計画


一見すると基本計画は有望に思えますが、詳しく調べてみると多くの落とし穴があります。

質問1:「URL(クエリを含む)+メソッド+本体」という式は、正確なマッチングには不十分です。例えば、同じAPIを2回リクエストしたが、戻り値が異なる場合、基本ソリューションに記録された情報では、これら2つのAPIリクエストを区別できません。例:ユーザーがアクティビティページに入り、API「a」を呼び出して登録ステータスを取得し、「未登録」を受け取り、登録ボタンを正常にクリックした後、再度API「a」を呼び出すと、「登録済み」を受け取ります。テストケースは、API「a」が呼び出されるたびに何を返すべきかをどのように判断すればよいでしょうか?

解決策:API呼び出しの入力パラメータは2つの呼び出し間で全く変化しないため、出力パラメータの変化を引き起こす可能性のある変化は何でしょうか?それは、アクションと時間です。そのため、各API呼び出しの情報を記録する際に、「データ属性」と「時間調整」も実行し、特定のユースケースノードに属性付けし、相対時間を記録します。例えば、API Aは「登録ボタンをクリックする」ノードに属性付けされており、約200ミリ秒の遅延があります。

質問 2: 一部のプロジェクトの API 入力パラメータには、フロントエンドによって生成されたランダムな情報が含まれているため、テストケースが実行されるたびに入力パラメータが異なり、不一致が発生します (これは「乱数サンドボックス」で解決できますが、まだ実装されていません)。ページの反復によって API 入力パラメータが変更され、不一致が発生する可能性があります。ページの反復後にランタイムが大幅に変更され、アトリビューション時間が一致しなくなる可能性があります。

解法:这些问题都是“精准匹配”导致的,其实解决思路与上文的目标元素定位类似,即增加“模糊匹配”。这里目前不需要LLM,因为匹配信息结构固定且可被量化,所以我们制定了"接口相似度匹配算法",根据url、query相似度(kv匹配、k匹配、多余的k等)、method、距归因节点时间等指标综合计算得分,选择得分最高的接口然后返回。(若无一个得分高的,则发往真实后端)

问题3:页面迭代后可能会引入新接口,该接口甚至可能跟用例所测模块毫无关系(所以正常来说,肯定不想因为它去更新用例),但因匹配不上,会发往后端,此时若无登录态会跳登录页或使页面出错,导致用例失败。

解法:用例录制时记录下cookies,用例执行时注入。这样通过”模糊匹配“也匹配不上的接口,会携cookies发往后端,至少大概率保证页面不报错。


如此,我们将“接口数据沙箱”划分为如下图所示的三层,每当上一层匹配失败时,触发下一层:



多环境与多用例并发执行



如图,由于需要支持在不同环境执行用例,所以我们将“用例执行”的部分单独拆了出来,作为“runner”,分别部署在不同环境,”runner“容器中也需预先装好无头浏览器、cypress和相关运行环境;将常规的crud部分作为”server“,同时它也负责将用例分发至对应环境的”runner“。

此外,每个用例的执行需占用一定资源和时间,执行器支持的最大用例并发数有上限,所以,用例进入runner时会进入队列中等候,在执行器有空余时执行。

主なプロセスは次のとおりです。

  1. 在用例录制时,获取每个url最终指向的ip地址,自动识别出当前运行环境,保存在用例中(也支持手动修改环境);

  2. 触发用例执行时,server将用例发往对应环境的runner执行;

  3. 用例进入runner的队列中等待执行。


DSL



我们测试框架代码之上定义了DSL层,它是对于用例的结构化描述,JSON格式,包含某个用例的全部信息。它的作用是:

  1. 便于用例录制时快速生成,便于存储;

  2. 便于可视化展示以及可视化编辑;

  3. 对用例进行结构上的约束,减小出错概率,收敛系统复杂度;

  4. 理论上可被转为各类测试框架代码并执行。


导出用例脚本并编辑


部分用例需高度定制,通过可视化编辑无法满足,因而我们提供了将用例DSL导出为cypress脚本的功能(后续考虑支持playwright脚本),通过修改cypress脚本便能实现一切cypress可实现的能力。

从上方“DSL“章节的图中可知,DSL可以转成cypress脚本代码,但该过程不可逆(DSL的能力是cypress脚本的子集),所以在用户导出cypress脚本后,该脚本会成为新的用例,作为”脚本类型用例“与”DSL类型用例“区分开来。

我们提供了cli工具,可在任意目录下初始化cypress项目,然后可将任意用例转为cypress脚本下载至此,以便在本地修改和运行调试,修改完成后可通过cli将用例上传至线上,成为一个新的”脚本类型用例“。”脚本类型用例“能够与”DSL类型用例“一样在线上执行。

流程示意如下:



見通し


现在已经实现了录制后自动生成用例脚本、代码小幅更新后自动修复用例脚本,但“录制”操作本身依然需要人工执行,而且在这之前还需制定用例,能否把这些过程也“自动”做掉?

一个直接的想法是:让LLM了解产品背景、阅读最新产品需求,生成用例。理论上让LLM生成文字描述的用例显然是可行的,而想要进一步生成用例DSL或用例脚本,还需使LLM知晓所有相关页面运行时的具体结构,如何运行、如何存储、如何理解、如何关联等都会是难题。

所以不如换一个角度:页面最终是服务于线上用户的,他们的操作本身不就是最好的”用例“么所以可以自动采集线上线上用户的真实操作链路,自动识别出高频链路、核心链路,自动归总为用例。相比之下,该方案更易实现(当然尚待深入的细节问题依然不少),且生成的用例质量甚至可能更好,同时也有借助LLM继续优化的可能性。

流程示意如下:



如此,在设定好策略后,便可自动批量产出用例、自动执行,做到“全流程全自动”,这才是真正“自动”的“自动化测试”。预期主要可以服务于数量庞大的、非核心的前端页面(数量太多难以一个个录入和维护,但又想一定程度上保证质量),大幅降低生产和维护用例的时间。