Upgrade to Pro — share decks privately, control downloads, hide ads and more …

生成AIを使ったテスト記述の最適化と生産性向上

 生成AIを使ったテスト記述の最適化と生産性向上

Ian Yamamoto

March 15, 2024
Tweet

More Decks by Ian Yamamoto

Other Decks in Technology

Transcript

  1. 自己紹介 山本 イアン (Ian Yamamoto) • 新卒でIT業界へ ◦ toBの社内向けシステム開発 ◦

    自治体向けパッケージソフトの保守運用など • SaaS系ソフトの自社サービス開発会社に転職 ◦ 開発以外に自動テストの導入を行う ◦ APIテストや静的解析、コーディング規約の導入なども • LegalOn Technologiesに入社 ◦ Software Engineer in Testとして活動を始め、今に至る
  2. 課題のおさらい: 1. そもそもテストに割けるリソースが十分ではない 2. 生成AIを導入しても一定の成果で止まりがち 3. どう書けばAIがテストコードを生成しやすいかという議論が発生しにくい これらの解決のために、 • どのようにテストを書いていくか

    • どのような補完が望ましいか • どう補完をチューニングしていくか という観点に絞り、私の経験に基づいて解決策の考察を発表します。 課題の再整理と目標の提示
  3. テストケースやビジネスロジックのサンプル 基本は結合テストメイン。 • APIテスト ◦ 正常系/異常系/複数系 • バッチ処理のテスト • AWS

    S3などの外部サービス連携のテスト 機能は基本的なWebアプリの機能 • ユーザー検索(管理者・企業管理者) • プロフィール機能 • メール通知機能
  4. 技術スタック 言語・フレームワーク:PHP 8.1 (Laravel 9.x), Intertia(Single Page Application) 0.6 テスト実行ツール:PHPUnit

    9.5 Linter / Formatter: PHP_CodeSniffer / PHP CS Fixer データベース:MySQL 8.x AIツール:GitHub Copilot / ChatGPT 3.5 - 4 ※プロジェクトの初期にTabnineを使用していましたが、トライアルなどの都合もありGitHub Copilotに移行しています。
  5. サンプルコードの前提 企業と一般ユーザーで何らかの交流ができるWebアプリ 企業ユーザー、一般ユーザー以外にもアプリの管理者がいる • 企業ユーザー … CompanyMemberテーブル • 一般ユーザー …

    Userテーブル この前提で、フィクスチャを用意後、管理者が企業ユーザーのテストコードを 手動で書き、その後に一般ユーザーの検索機能のテストを書く。 ⼀般ユーザーの検索機能のテストから「⼤きな補完」が出始めるはず
  6. テスト対象コード(企業ユーザー検索) public function index(Request $request) { $keyword = $request->search_text ??

    ''; $companyMembers = CompanyMember::where('name', 'like', '%' . $keyword . '%') ->orderBy('updated_at', 'DESC') ->paginate(10) ->withQueryString(); // 更新すべきデータとともにページをバックエンドから返す return Inertia::render('Admin/CompanyMember/Index', [ 'companyMembers' => $companyMembers, ]); } リクエストからもらった名前でLIKE検索をするシンプルな実装。
  7. テストクラスやフィクスチャを作成 class AdminCorporateSearchTest extends TestCase { // 毎テストごとにデータベースをリセット use RefreshDatabase;

    // ログインするユーザー private $admin; // 管理者作成フィクスチャ protected function setUp() : void { // PHPUnitのsetUpメソッドを呼び出し、テスト環境を初期化 parent::setUp(); // 管理者ユーザーをDBに作成 $this->admin = AdminUser::factory()->create([ 'user_div' => Const::USER_ADMIN, ]); // 作成した管理者ユーザーでログイン $this->actingAs($this->admin); } テスト毎にDBを初期化し、管理者としてログインするシンプルなフィクスチャ 最初は小さな補完から。 解説のため毎行コメント記述、 改行少なめとなっています。 実際のコメントは 「管理者作成フィクスチャ」 のみですが、重要です(後述)
  8. テストコード(企業ユーザー検索) 1. テストデータの作成 a. 田中 太郎 b. 田中 花子 c.

    山田 次郎 2. 「田中」で検索 a. 田中太郎、田中花子がヒットし ます 3. レスポンスをアサート a. 指定したページを返したか b. ヒット数が2名か c. それぞれのIDをベースにヒット した名前が正しいか public function test_search_company_members_by_name () { // テストデータの準備 $member1 = CompanyMember::factory()->create(['name' => '田中 太郎 ']); $member2 = CompanyMember::factory()->create(['name' => '田中 花子 ']); $member3 = CompanyMember::factory()->create(['name' => '山田 次郎 ']); // 検索キーワード「田中」でフィルタリングされた結果をテスト $response = $this->get(route('admin.corporate.index', ['search_text' => '田中'])); $response->assertInertia(fn ($page) => $page ->component('Admin/CompanyMember/Index') ->where('companyMembers.data', fn ($members) => count($members) === 2 && collect($members)->pluck('id')->contains($member1->id) && collect($members)->pluck('id')->contains($member2->id) && !collect($members)->pluck('id')->contains($member3->id)) ); } 注:DocStringは省略しています
  9. テスト対象コード(一般ユーザー検索) public function index(Request $request) { $keyword = $request->search_name ??

    ''; $members = User::where('name', 'like', '%' . $keyword . '%') ->orderBy('updated_at', 'DESC') ->paginate(10) ->withQueryString(); return Inertia::render('Admin/User/Index', [ 'users' => $members, ]); } 参照先のみが変わる実装。次にAIをつかっていきます。 ※手動で実装
  10. 生成されたテストコード(一般ユーザー検索) public function test_search_users_by_name() { $user1 = User::factory()->create(['name' => '田中

    太郎']); $user2 = User::factory()->create(['name' => '田中 花子']); $user3 = User::factory()->create(['name' => '山田 次郎']); $response = $this->get(route('admin.user.index', ['search_text' => '田中'])); $response->assertInertia(fn ($page) => $page ->component('Admin/User/Index') ->where('users.data', fn ($users) => count($users) === 2 && $users[0]['name'] === '田中 太郎' && $users[1]['name'] === '田中 花子' ) ); } 前のテストコードと違う点 • テスト名 • テストデータ • ルート名 • アサート方法 それ以外は同じ。 テストデータは書き方が同じであ ることが重要 生成したテストが正しいことより、テストの流れを再現できたことが重要
  11. 複雑なテストは先に書いたほうが補完が効きやすい 下記のようなテストの書き方は、AIにとって複雑なテストとなりがち。 • 単数形 → 複数形 • 任意のオプションがないケース → 任意を含めたケース

    • ハッピーパス → 網羅的なパス • ページネーションなし → ページネーションあり これらは、逆の順番で書いたほうがAIにとっては提案しやすい 常にAIへ”全体像”を見せてくれるケースを優先して書く
  12. フィクスチャを「大きな補完」へ昇華させる例 それぞれのフィクスチャにコメントを つける。 例のように、フィクスチャをコメント で置いても、別ファイルで実装部分の みをコメントアウトした状態ですべて の⾏を出⼒できる。 更に、テストクラス名にNotification があれば通知フィクスチャを提案す る。

    タグをつけるイメージで記述する。 class AdminUserProfileTest extends TestCase { … // 管理者作成フィクスチャ protected function setUp() : void { parent::setUp(); $this->admin = AdminUser::factory()->create([ 'user_div' => Const::USER_ADMIN, ]); $this->actingAs($this->admin); } // // 通知を受け取るユーザー作成フィクスチャ // protected function setUp() : void // { // parent::setUp(); // Notification::fake(); // $this->admin = AdminUser::factory()->create([ // 'user_div' => AppConst::USER_DIV_ADMIN, // ]); // $this->actingAs($this->admin); // } プロンプト(⻑めの⽂章)を書いて⽣成するというよりは、 正確にタグ(短い⽂章や単語)の実装を繰り返して認識させるイメージがいい
  13. APIペイロードを補完する例 /* API Payload companies.data companies.current_page 1 companies.per_page 6 companies.last_page

    companies.total */ … public function test_companies_are_displayed_correctly () { $response = $this->get(route('admin.company.index' )); $response->assertInertia(fn (AssertableInertia $page) => $page ->component('Admin/Company/Index' ) ->has('companies.data', 3) // API Payload ->where('companies.data', 3) ->where('companies.current_page' , 1) ->where('companies.per_page' , 6) ->where('companies.last_page' , 1) ->where('companies.total', 3) ); } APIペイロードのプロンプトを仕込 み、テストを⽣成 実際は1⾏ずつの補完となるため、 ⼤きい補完への繋ぎとして利⽤す るのがよい 作成後コメント部分は削除推奨 DBも基本は同じ
  14. 過剰な最適化を避ける • ルーティング情報は埋もれがち ◦ クロージャなどは便利だが、⽣成AIにとってコンテキストが離れがち ▪ その際はプロンプトで表などを仕込むか⼿動でコーディングするほう が効率的 ▪ そもそもテスト関数名以外はあまりみていない可能性が⾼い

    ▪ 検索画⾯=searchのように、どうしても関連する単語を使いたがる • クレデンシャル情報は⽣成が途中で途切れる ◦ 順番を変えられるなら最後の⽅に持っていく ◦ 専⽤の定数を持つと途切れることを回避できる ▪ TEST_EMAIL, TEST_PASSWORDなどを使う コピペや⼿⼊⼒など、他の代替案が早いかを常に意識する。
  15. 対話型AIについて • メリット ◦ 細かい条件を調整しやすい ▪ その過程をプロンプト化できる ◦ 共有ができる ◦

    質問やエラー解決も可能 • デメリット ◦ 応答までに時間がかかる ◦ 機密情報を書いてしまう可能性がある ◦ チャットの⻑さや頻度に上限を設けているケースがある 対話型AIでも⼤きな補完を意識する
  16. 1.他の作業者が同じようにテスト⽣成したいと思った場合、資産化したプロンプトの構 ⽂を真似して⼊⼒してもらうイメージでしょうか? その通りです。厳密にはGitHub Copilotでは、プロンプトだけではなくこれまでの実 装と、どういう提案を受け⼊れ/拒否したかが重要な資産となります。 なのでGitHub Copilotでいえば、他の作業者がテストを作成し、Copilotに既存の実装 などの資産を参考に⼊⼒してもらい、修正があれば⾏っていくイメージです。 2.プロダクトコードやコメントを元にしたテストコードになると、テストとして成⽴し ないのではないのでしょうか?

    プロダクトコードの正当性は補完戦略とは別に検証する必要があります。 この発表はテストコード記述の⽣産性向上にフォーカスしておりますが、コード補完 のみでテストが完結することを推奨しているわけではありません。 どのようなコードを補完すればいいのか?という問題はそれぞれの開発者に委ねられ ているのがコード補完の現状であり、⽣成AIは良くも悪くもコードを沢⼭書けるツー ルである、という意識が重要です。