
AIが生成したテストコードを負債にしないためのレビュー観点
こんにちは。リテールアプリ共創部マッハチームのきんじょーです。
マッハチームでは小売業界のお客様に向け、新規アプリ開発の立ち上げを専門に行なっています。
新規開発の立ち上げでは、短期間で高品質なアプリケーションを開発することが求められます。
マッハチームのメンバーで「品質」をテーマに知見を共有するブログの連載を始めました。
今回は私から、第3弾としてAIが生成したテストコードを負債にしないためのレビュー観点をご紹介します。
メンバーによる過去の投稿もぜひ読んでみてください!
テストコード実装時に気をつけていること
最近のAIコーディングエージェントでは、テストコードの生成だけでなく、テストの実行、エラー原因の調査、バグ修正までを自律的に行ってくれます。
少しの指示を出すだけで大量のテストコードを生成し、テストが通る状態まで一瞬で実装することができます。
しかし、AIが生成した大量のテストコードをそのまま採用してしまうと、その後の保守性を考えた際にコードリーディングの負荷や変更時のコストが上がり負債になってしまいます。
この記事ではAIが生成したコードをレビューする際の観点として気をつけていることをTips形式でご紹介します。
セットアップ処理の共通化
AIでテストコードを生成すると、テストに必要なデータの用意や事前条件のセットアップ処理を各テストケース内で愚直に組み立てたコードが大量に出てきがちです。
テストとしては動くのですが、保守性の面では辛くなります。
例えば、組織情報の取得処理をテストしているとして、以下のようにテストケースごとにダミーデータを定義してしまうコードを例に挙げます。
it("組織情報を取得できる", async () => {
const organization: Organization = {
id: "test-organization-id",
name: "Test Organization",
organizationId1: "organization-id-1",
organizationId2: "organization-id-2",
// ... 大量のプロパティ ...
};
// ... テスト本体 ...
});
この状態で Organization に必須項目が追加されたり、型が変更されたりすると、影響を受けるテストケースを全て探して直す必要が出てきます。
このような場合、Fixture関数を用意して複数のテストケースで使い回すように指示しています。
ベースとなるオブジェクトはFixture側で用意し、テストごとに変えたい値だけを Partial<T> で上書きできる形にしておくと便利です。
// domain/organization.dummy.ts
/**
* Organizationオブジェクトのダミーを生成する
*/
export const organizationDummyFrom = (params?: Partial<Organization>): Organization => {
return {
id: params?.id ?? "test-organization-id",
name: params?.name ?? "Test Organization",
organizationId1: params?.organizationId1 ?? "組織1",
organizationId2: params?.organizationId2 ?? "組織2",
// ... 大量のプロパティ ...
};
};
テスト側は以下のように「何を上書きしているか」だけが目に入るようになります。
import { organizationDummyFrom } from "../domain/organization.dummy";
it("組織名を返す", async () => {
const organization = organizationDummyFrom({ name: "mach-team" });
// ... テスト本体 ...
});
Fixtureを導入すると、ダミーデータの作成処理が集約され型変更による修正漏れが減ります。
また、テストコードが「テスト意図」に寄って読みやすくなるのもメリットです。
AIにテストコード生成を任せる場合も、ここはルールとして明文化しておくのがおすすめです。
テストデータの生成(ルール例)
テストデータは*.dummy.tsの命名規則でファイルを作成し、ダミーのファクトリー関数を用意します。
// Example: organization.dummy.ts
export const organizationDummyFrom = (params?: Partial<Organization>): Organization => {
return {
id: params?.id ?? "test-organization-id",
name: params?.name ?? "Test Organization",
};
};
各テストケースで冗長にダミーデータを定義せずに、共通のダミーファクトリーを使用して生成します。
const mockOrganization = organizationDummyFrom({
id: "organization-id-1",
name: "mach-team",
});
不要なテストケースの削除
AIにテストコード生成を任せると、分岐網羅・境界値・異常系を「全部入り」で網羅しようとして、テストケースが増えすぎることがあります。
その結果、レビューコストが上がるだけでなく、変更に弱いテストが増えて負債になりがちです。
以下のような観点で不要なテストケースを間引いています。
- 他のテストで担保できている: 同じ仕様を別レイヤー(単体テスト/結合テスト/E2Eテスト)で既に担保しているなら、重複は消します(片方だけ残す)
- 責務の境界が違う: そのテスト対象の責務ではないケースは消します(例: 入力バリデーションはAPI層、ドメインは不変条件を前提、など)
- 前提(契約)で起きない: 「空文字は来ない」「配列は必ず1件以上」など、仕様・スキーマ・DB制約で保証されているなら、その前提を再現するテストは基本的に書きません
- ビジネス上クリティカルではない: 起きても影響が軽微で、かつ起きにくい異常系のケースは優先度を下げて削ります(残すなら“なぜ必要か”をコメントで説明できるものだけ)
例えばAIが以下のように境界値テストを大量生成してきた場合を考えます。
describe("formatOrganizationName", () => {
it("空文字の場合は空文字を返す", () => {/* ... */});
it("空白のみの場合は空文字を返す", () => {/* ... */});
it("nullの場合は例外を投げる", () => {/* ... */});
it("undefinedの場合は例外を投げる", () => {/* ... */});
it("長さ1の場合", () => {/* ... */});
it("長さ255の場合", () => {/* ... */});
it("長さ256の場合", () => {/* ... */});
});
この中で本当に守りたい仕様が「表示名が正しく整形されること」だけなら、テストは最小限に寄せます。
例えば「通常ケース」「仕様上あり得る入力の代表ケース」だけに絞る、という判断です。
describe("formatOrganizationName", () => {
it("組織名を表示用に整形できる", () => {/* ... */});
it("ハイフン区切りのチーム名を表示用に変換できる", () => {/* ... */});
});
上記を守るためのAIへの指示を考えてみます。
例えば「網羅率を上げる目的での境界値テストは禁止」「各テストケースに“守りたい仕様”を1行で添える」をルールにしておくと、不要なケースが出にくくなります。
## テストケースの取捨選択ルール
- 分岐網羅/境界値を目的にテストケースを増やさない
- 追加するテストケースには「何の仕様を守るためか」を1行で書く
- 他のレイヤーのテストで担保できているなら重複したテストケースは作らない(片方に寄せる)
Enum/定数を使って直書きしない
AIが書いたテストコードを見ていて、割と見落としがちなのが値の直書き(マジックナンバー/マジックストリング)です。
ダミーデータの作成やテスト結果のアサーションなどで、事前に定義されているEnum(または定数)を使わずに、文字列や数値を直書きしてくることがあります。
例えば以下のようなコードです。
it("有効な組織の場合は200を返す", () => {
const organization = organizationDummyFrom({
// 既にEnum/定数があるのに、値を直書きしてしまう
status: "ACTIVE",
type: "retail",
});
const result = validateOrganization(organization);
expect(result.code).toBe(200);
expect(result.status).toBe("ok");
});
テストとしては動くのですが、後からリファクタリングで名称や値が変わると、直書きしている箇所を探して修正する必要が出てきます。
このような場合は、事前定義したEnum/定数を使うように指示します。
it("有効な組織の場合は200を返す", () => {
const organization = organizationDummyFrom({
status: OrganizationStatus.Active,
type: OrganizationType.Retail,
});
const result = validateOrganization(organization);
expect(result.code).toBe(HttpStatus.OK);
expect(result.status).toBe(ResultStatus.Ok);
});
同じようなテストをテーブル駆動にまとめる
AIが生成したテストコードでは、似たようなテストケースが少しずつ条件だけ変えて大量に並ぶことがあります。
こういうケースは it.each を使ってテーブル駆動にまとめて、見通しをよくするよう指示しています。
describe("formatOrganizationName", () => {
it.each([
["mach-team", "mach team"],
["mach_team", "mach team"],
[" Mach Team ", "mach team"],
])("input=%s の場合 %s を返す", (input, expected) => {
expect(formatOrganizationName(input)).toBe(expected);
});
});
テスト対象コードの整理
テスト対象の関数が複数の責務を持っていると、そもそもテストが書きづらくなります。
テストに必要な条件を揃えるのが大変だったり、テスト結果のアサーションが複雑になったりして、テストの意図が読み取りにくくなるためです。
ただ最近はAIを使うと、そういったテスタビリティの低いコードでも、力技でテストケースを書けてしまいます。
そして「テストは通る」ので、そのまま採用してしまいがちです。
しかし、そのようなテストコードは壊れやすく、読みにくく、変更しづらいことが多いです。
テスト対象の関数が少し変わっただけで大量のモックや前提条件が崩れ、テストが一斉に壊れて修正コストが跳ね上がってしまいます。
ここで意識しているのは、テストコードを書く前に「テスト対象の関数の責務を単一にする」ことです。
入出力が明確で、依存が少ない形に整えておくと、テストは自然と短くなり、アサーションもシンプルになります。
AIにテストコードの生成を任せる場合も「まずテスト対象コードの整理から入る」ルールにしておくと、結果的にコードもテストも読みやすくなります。
まとめ
今回は、AIが生成したテストコードを負債にしないためのレビュー観点を、Tips形式でご紹介しました。
上記には挙げていませんが、AIが生成したコードのレビュー負荷をコントロールして見落としを防ぐために、一度に大量にコードを生成しすぎない点も意識しています。
AIが生成したコードのレビューを怠ってしまうと、そのままチームメンバーのレビュー負荷につながってしまうので、今後も意識していきたいです。
この記事がどなたかの役に立つと幸いです。
以上。リテールアプリ共創部のきんじょーでした。
エンジニア募集のお知らせ
リテールアプリ共創部マッハチームではエンジニアを大募集しています!
マッハチームでは 0→1 の新規案件の立ち上げを専門に、AI駆動で高速にプロダクトを開発しています。
AI駆動開発がしたい方、モダン技術を用いてフロントエンドもサーバーサイドもインフラもTypeScriptでフルスタックに開発したい方、プリセール・顧客折衝も要件定義も開発も全部やりたい方は、是非以下のブログも読んでみてください。
お気軽にカジュアル面談もお待ちしています!








