Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
AI 時代だからこそ抑えたい「価値のある」PHP ユニットテストを書く技術 #phpconfu...
Search
shogogg
November 08, 2025
Programming
1
74
AI 時代だからこそ抑えたい「価値のある」PHP ユニットテストを書く技術 #phpconfuk / phpcon-fukuoka-2025
2025年11月8日に開催されたPHPカンファレンス福岡2025の登壇資料です。
shogogg
November 08, 2025
Tweet
Share
More Decks by shogogg
See All by shogogg
PHPに関数型の魂を宿す〜PHP 8.5 で実現する堅牢なコードとは〜 #phpcon_hiroshima / phpcon-hiroshima-2025
shogogg
1
380
PHP開発者のためのSOLID原則再入門 #phpcon / PHP Conference Japan 2025
shogogg
6
1.6k
PHPer のための プロポーザル駆動アウトプット入門 #phpcon_niigata / PHP Conference Niigata 2025
shogogg
1
480
技術的負債を正しく理解し、正しく付き合う #phperkaigi / PHPerKaigi 2025
shogogg
7
2.6k
5分で理解する SOLID 原則 #phpcon_nagoya
shogogg
1
770
パスワードよもやま話
shogogg
1
340
readonly class で作る堅牢なアプリケーション
shogogg
2
2.1k
Other Decks in Programming
See All in Programming
Kotlinで実装するCPU/GPU 「協調的」パフォーマンス管理
matuyuhi
0
280
組織もソフトウェアも難しく考えない、もっとシンプルな考え方で設計する #phpconfuk
o0h
PRO
3
680
自動テストのアーキテクチャとその理由ー大規模ゲーム開発の場合ー
segadevtech
0
480
Node-REDのノードの開発・活用事例とコミュニティとの関わり(Node-RED Con Nagoya 2025)
404background
0
120
テーブル定義書の構造化抽出して、生成AIでDWH分析を試してみた / devio2025tokyo
kasacchiful
0
380
CSC305 Lecture 13
javiergs
PRO
0
360
釣り地図SNSにおける有料機能の実装
nokonoko1203
0
200
Claude Agent SDK を使ってみよう
hyshu
0
1.5k
Register is more than clipboard
satorunooshie
1
390
開発組織の戦略的な役割と 設計スキル向上の効果
masuda220
PRO
10
2.1k
AIのバカさ加減に怒る前にやっておくこと
blueeventhorizon
0
150
なんでRustの環境構築してないのにRust製のツールが動くの? / Why Do Rust-Based Tools Run Without a Rust Environment?
ssssota
14
47k
Featured
See All Featured
How GitHub (no longer) Works
holman
315
140k
Designing Experiences People Love
moore
142
24k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
23
1.5k
Making Projects Easy
brettharned
120
6.4k
Scaling GitHub
holman
463
140k
Docker and Python
trallard
46
3.6k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
249
1.3M
A Tale of Four Properties
chriscoyier
161
23k
Building Flexible Design Systems
yeseniaperezcruz
329
39k
Making the Leap to Tech Lead
cromwellryan
135
9.6k
The Art of Programming - Codeland 2020
erikaheidi
56
14k
Reflections from 52 weeks, 52 projects
jeffersonlam
355
21k
Transcript
Shogo Kawase / @shogogg Nov. 8 2025 PHP Conference Fukuoka
2025 AI 時代だからこそ抑えたい 「価値のある」PHP ユニットテストを書く技術
テストコード書いてますかー?
None
価値の高いテストコードを書く人を増やす Today’s Goal
自己紹介 河瀨 翔吾 / Shogo Kawase 株式会社 PR TIMES ソフトウェアエンジニア(2024.12〜)
I LOVE... 妻 / 型安全 / アジャイル / ももいろクローバーZ F1 / マリオカート shogogg shogogg
We are Hiring!!
おしながき 1 AI時代におけるユニットテストの価値とは 2 価値のあるテストコードを書くための基本的な考え方 3 よくあるアンチパターンと処方箋・テクニック 4 まとめ
おしながき 1 AI時代におけるユニットテストの価値とは 2 価値のあるテストコードを書くための基本的な考え方 3 よくあるアンチパターンと処方箋・テクニック 4 まとめ
AIエージェントの普及によって 何が変わったのか
鹿野壮(Ubie 株式会社)「Claude Codeが刷新した開発現場と、人間に残された仕事」より
AIエージェントの普及による変化 アウトプットが爆増 • AIエージェントが自律的に作業することでアウトプット≒プルリクが爆増中 • プルリクの増加に伴い、レビューの負担も増加 テストコードの作成コストは激減 • 熟練の職人が手作りする時代は終わり、大量生産の時代へ •
複雑なモジュールのテストも、テスト設計の段階からAIの支援が受けられる 自動テストの重要性は増加 • 大量の変更を安全にリリースするため、自動化の重要性は増している • AI による誤った変更を検知するための「ガードレール」としての価値
じゃあ、もうテストコードは全部 AI に丸投げでいいよね? AI に任せるなら、読みやすさなんか無視して動けば OK じゃね?
じゃあ、もうテストコードは全部 AI に丸投げでいいよね? AI に任せるなら、読みやすさなんか無視して動けば OK じゃね?
AI時代におけるユニットテストの価値 高い生産性と安全性を両立させる「安全装置」 • AI による圧倒的な量のアウトプットをすべて人力でテストするのは無理 • 自動テストで品質を担保することで、高速かつ安全なリリースが可能に AI の暴走を抑える「ガードレール」 •
AI は放っておくと暴走しておかしなことをすることもまだ少なくない • AI が既存の仕様を破壊していないか、自動で検知する仕組みが必要 人間と AI の協働を助ける「動く仕様書」 • ユニットテストの「実行可能なドキュメント」としての価値は依然高いまま • 読みづらいテストコードは誤ったコンテキストを伝え、人間や AI がバグを生む原因に
おしながき 1 AI時代におけるユニットテストの価値とは 2 価値のあるテストコードを書くための基本的な考え方 3 よくあるアンチパターンと処方箋・テクニック 4 まとめ
ユニットテストが抑えるべき4つのポイント すばやく実行でき、すぐに結果がわかる 高速性 偽陽性や偽陰性が発生せず、結果を信頼できる 信頼性 いつ・どこで・誰が実行しても同じ結果になる 移植性 人間と AI の両方にとって読みやすい
可読性
高速性 — すばやく実行でき、すぐに結果がわかる • 遅いテストは生産性と開発者体験を大きく下げる • AI が爆速で実装してくれても、遅いテストがボトルネックに • テスト実行によるフィードバックループを高速に回せることが、高い生産性につながる
信頼性 — 偽陽性や偽陰性が発生せず、結果を信頼できる • Flaky なテストや壊れやすいテストがあると結果が信頼できなくなる • 信頼できるテスト があるからこそ、安心して作業を AI
に任せたり、リファクタリング ができる
• テストはいつ・どこで実行されるかわからない 例)ローカル / CI / LLM Agent on Web
/ 隔離されたコンテナ内など • 特定の環境になるべく依存しない「どこでも動くテスト」の必要性が従来よりもさらに 高まっている 移植性 — いつ・どこで・誰が実行しても同じ結果になる
可読性 — 人間と AI の両方にとって読みやすい • テストコードは「動く仕様書」 • 読みにくいテストコードはレビューやリファクタリングのコストを増やすだけでなく AI
が間違ったコードを生成する原因 にもなる • 読みやすいテストコードは AI や人間のコンテキスト消費を抑え、高い生産性を実現 し それが経済的なインパクト に繋がる
おしながき 1 AI時代におけるユニットテストの価値とは 3 よくあるアンチパターンと処方箋・テクニック 4 まとめ 2 価値のあるテストコードを書くための基本的な考え方
おことわり • ここから紹介するアンチパターンはすべて私が通ってきた道 であり先人・先達の知恵 から学び、今では避けているパターンです 😖 • 個人的な主義・主張が反映されている場合があります 😋 •
あらゆるアンチパターンを網羅したものではありません • アンチパターンの「名前」はおおむね私の創作なので現場で急に使うことはオススメ しません 😇
1. 英語至上主義
#[Test] public function isEvolvableShouldReturnTrueWhenThePokemonCanEvolve(): void { $pokemon = new Pokemon(
name: 'ピカチュウ', evolveTo: ['ライチュウ'], // ... ); self::assertTrue($pokemon->isEvolvable()); } 1. 英語至上主義
1. 英語至上主義 • 変数やクラス名同様、テストケースを英語で書きたがる ※ BDD スタイルのフレームワークの場合にテストメソッドが it だったりする等、英語で書きたくなる気持ちはわかる 症状
• 非英語話者が英文を考える労力 • 英単語や英文法の間違いなど、本質的でないレビューのやり取りが増える • 複雑なドメイン用語を英語にすると長くなる問題 • シンプルに読みづらい 問題点
1. 英語至上主義 処方箋 社内公用語(≒日本語)で書く
#[Test] public function isEvolvableShouldReturnTrueWhenThePokemonCanEvolve(): void { $pokemon = new Pokemon(
name: 'ピカチュウ', evolveTo: ['ライチュウ'], // ... ); self::assertTrue($pokemon->isEvolvable()); } Before
#[Test] public function isEvolvable_そのポケモンが進化できる場合は_true_を返す(): void { $pokemon = new Pokemon(
name: 'ピカチュウ', evolveTo: ['ライチュウ'], // ... ); self::assertTrue($pokemon->isEvolvable()); } After
2. 勘違いDRY原則
// テストクラス内で共通のプロパティ private $pokemon = [ new Pokemon(name: 'フシギダネ', types:
['くさ', 'どく']), // ... ]; // ... #[Test] public function hasSameType_指定されたポケモンと同じタイプを持つ場合は_true_を返す(): void { self::assertTrue($this->pokemons[0]->hasSameType($this->pokemons[21])); } 2. 勘違いDRY原則
2. 勘違いDRY原則 • DRY原則を「コードの重複をなくす」ものと勘違いして、テストコードでもどんどん コードを共通化したり、制御構文を多用する 例)共通のデータ、共通化されたメソッド、過剰な変数化、実装の参照など 症状 • テスト内容・事前条件・事後条件 がコードのあちこちを見ないとわからないため、
「動く仕様書」 としての価値が低下 • 共通化した部分の責務がどんどん増えてしまい、予期せぬバグにつながる 問題点
2. 勘違いDRY原則 処方箋 DAMP原則
DAMP原則? • Descriptive And Meaningful Phrases • 説明的で意味のある 記述 •
コードの重複を受け入れ、一目見ただけで意味の分かる記述を重視 • 「動く仕様書」 であるテストコードではDRY原則よりもDAMP原則を守った方がよい
// テストクラス内で共通のプロパティ private $pokemon = [ new Pokemon(name: 'フシギダネ', types:
['くさ', 'どく']), // ... ]; // ... #[Test] public function hasSameType_指定されたポケモンと同じタイプを持つ場合は_true_を返す(): void { self::assertTrue($this->pokemons[0]->hasSameType($this->pokemons[21])); } Before
#[Test] public function hasSameType_指定されたポケモンと同じタイプを持つ場合は_true_を返す(): void { $bulbasaur = new Pokemon(
name: 'フシギダネ', types: ['くさ', 'どく'], // ... ); $gengar = new Pokemon( name: 'ゲンガー', types: ['ゴースト', 'どく'], // ... ); self::assertTrue($bulbasaur->hasSameType($gengar)); } After
「テストの知識」を共通化する以下のようなケースは有用 • テスト用のデータを生成するファクトリクラス or メソッド • 複雑、かつコードから意図が読み取りづらい assertion をシンプルかつ読みやすくする ための
custom assertion テストコードでDRYは常に悪なのか?
#[Test] public function hasSameType_指定されたポケモンと同じタイプを持つ場合は_true_を返す(): void { $a = TestPokemonFactory::make( types:
['くさ', 'どく'], ); $b = TestPokemonFactory::make( types: ['ゴースト', 'どく'], ); self::assertTrue($a->hasSameType($b)); } テスト用ファクトリクラスの例
3. テストケースプロバイダー
public function provideTestCases(): iterable { yield '同じタイプを持つポケモンの場合は true を返す' =>
[ 'pokemon1' => new Pokemon(/* ... */), 'pokemon2' => new Pokemon(/* ... */), 'expected' => true, ]; yield '同じタイプを持たないポケモンの場合は false を返す' => [ // ... ]; } #[Test] #[DataProvider('provideTestCases')] public function hasSameType_引数を判定して結果を返す(Pokemon $a, Pokemon $b, bool $expected): void { self::assertSame($expected, $a->hasSameType($b)); } 3. テストケースプロバイダー
3. テストケースプロバイダー • テストコードの共通化を目的に、データプロバイダーをテストケースの記述に流用して いる(勘違いDRY原則の亜種) 症状 • テストコードが「プロバイダから提供されたデータでテストするためのプログラム」 となってしまい、「動く仕様書」 としての価値が低下
• テストが意図通りに動いていない場合などのデバッグが困難に 問題点
3. テストケースプロバイダー 処方箋 データプロバイダーで テストケースを書かない
public function provideTestCases(): iterable { yield '同じタイプを持つポケモンの場合は true を返す' =>
[ 'pokemon1' => new Pokemon(/* ... */), 'pokemon2' => new Pokemon(/* ... */), 'expected' => true, ]; yield '同じタイプを持たないポケモンの場合は false を返す' => [ // ... ]; } #[Test] #[DataProvider('provideTestCases')] public function hasSameType_引数を判定して結果を返す(Pokemon $a, Pokemon $b, bool $expected): void { self::assertSame($expected, $a->hasSameType($b)); } Before
#[Test] public function hasSameType_同じタイプを持つポケモンの場合は_true_を返す(): void { $a = new Pokemon(name:
'フシギダネ', types: ['くさ', 'どく']); $b = new Pokemon(name: 'ゲンガー', types: ['ゴースト', 'どく']); self::assertTrue($a->hasSameType($b)); } #[Test] public function hasSameType_同じタイプを持たないポケモンの場合は_false_を返す(): void { $a = new Pokemon(name: 'フシギダネ', types: ['くさ', 'どく']); $b = new Pokemon(name: 'プリン', types: ['ノーマル', 'フェアリー']); self::assertFalse($a->hasSameType($b)); } After
4. 言葉足らずのテスト
#[Test] public function doAction_正常系(): void { $action = new ReplacePokemonAction();
$actual = $action->doAction(new Pokemon(name: 'ゼニガメ', status: Pokemon::ACTIVE)); self::assertTrue($actual); } #[Test] public function doAction_異常系(): void { $action = new ReplacePokemonAction(); $actual = $action->doAction(new Pokemon(name: 'カイリュー', status: Pokemon::FAINTING)); self::assertFalse($actual); } 4. 言葉足らずのテスト
4. 言葉足らずのテスト • テストケース(テストメソッド名)に条件や期待値などが記述されておらず「正常系」 や「成功すること」 などの短い記述になっている 症状 • 事前条件や事後条件などをコードから読み取る必要があり「動く仕様書」 としての価
値が大きく低下 • 人間や AI が読んだときの情報が不足することで、コードの修正時やレビュー時の手間 が増え、精度が落ち、生産性が下がる 問題点
4. 言葉足らずのテスト 処方箋 テストケースには「ふるまい」を書く
#[Test] public function doAction_正常系(): void { $action = new ReplacePokemonAction();
$actual = $action->doAction(new Pokemon(name: 'ゼニガメ', status: Pokemon::ACTIVE)); self::assertTrue($actual); } #[Test] public function doAction_異常系(): void { $action = new ReplacePokemonAction(); $actual = $action->doAction(new Pokemon(name: 'カイリュー', status: Pokemon::FAINTING)); self::assertFalse($actual); } Before
#[Test] public function doAction_指定されたポケモンが元気な場合は成功し_true_を返す(): void { $action = new ReplacePokemonAction();
$actual = $action->doAction(new Pokemon(name: 'ゼニガメ', status: Pokemon::ACTIVE)); self::assertTrue($actual); } #[Test] public function doAction_指定されたポケモンが「ひんし」の場合は失敗し_false_を返す(): void { $action = new ReplacePokemonAction(); $actual = $action->doAction(new Pokemon(name: 'カイリュー', status: Pokemon::FAINTING)); self::assertFalse($actual); } After
5. 欲張りなテスト
#[Test] public function doAction_バリデーションエラーがない場合は処理に成功する(): void { $request = new ReplacePokemonActionRequest(
// ... ); $actual = $this->action->doAction($request); self::assertSame(Response::HTTP_OK, $actual->getStatusCode()); $json = json_decode((string)$actual->getContent(), true); self::assertArrayHasKey('message', $json); self::assertSame('ポケモンの交代に成功しました。', $json['message']); self::assertArrayHasKey('from', $json); self::assertSame('フシギバナ', $json['from']); self::assertArrayHasKey('to', $json); self::assertSame('カメックス', $json['to']); } 5. 欲張りなテスト
5. 欲張りなテスト • 1つのテストケースで複数のふるまい・事後条件をテストしてしまう 症状 • 複数のふるまいをテストするため、結果的にテストケース名がふわっとしてしま い「言葉足らず」 に •
複数のアサーションの先頭や途中で検査に失敗するとそこでテストの実行が中断 されてしまうため、後続の検査が行われず、問題発生時の調査が困難に 問題点
5. 欲張りなテスト 処方箋 1つのテストケースでは 1つのふるまいだけをテストする
#[Test] public function doAction_バリデーションエラーがない場合は処理に成功する(): void { $request = new ReplacePokemonActionRequest(
// ... ); $actual = $this->action->doAction($request); self::assertSame(Response::HTTP_OK, $actual->getStatusCode()); $json = json_decode((string)$actual->getContent(), true); self::assertArrayHasKey('message', $json); self::assertSame('ポケモンの交代に成功しました。', $json['message']); self::assertArrayHasKey('from', $json); self::assertSame('フシギバナ', $json['from']); self::assertArrayHasKey('to', $json); self::assertSame('カメックス', $json['to']); } Before
#[Test] public function doAction_成功時はステータスコードが_200_OK_である(): void { $request = new ReplacePokemonActionRequest(/*
... */); $actual = $this->action->doAction($request); self::assertSame(Response::HTTP_OK, $actual->getStatusCode()); } #[Test] public function doAction_成功時は本文に交代前のポケモン名が含まれる(): void { $request = new ReplacePokemonActionRequest(/* ... */); $actual = $this->action->doAction($request); $json = json_decode((string)$actual->getContent(), true); self::assertArrayHasKey('from', $json); self::assertSame('フシギバナ', $json['from']); } After
6. スパゲッティ・テスト
#[Test] public function fetchMyTeam_APIが_200_OK_を返す場合は手元のポケモン一覧を返す(): void { $httpClientMock = Mockery::mock(HttpClient::class); $httpClientMock
->expects('get') ->with('/api/v2/my-team') ->andReturn(new Response(200, [], json_encode([ ['name' => 'プリン', 'level' => 88, /* ... */], // ... ]))); $subject = new PokemonService($httpClientMock); $expected = [ new Pokemon(name: 'プリン', level: 88, /* ... */), // ... ]; $actual = $subject->fetchMyTeam(); self::assertEquals($expected, $actual); } 6. スパゲッティ・テスト
6. スパゲッティ・テスト • 複雑な事前条件や assertion がある場合など、テストコードが長くゴチャゴチャとして しまい、どこで何をしているのかわかりにくくなってしまう 症状 • テストコードを読むことが困難・面倒になってしまい「動く仕様書」
としての利便性 が著しく低下 • 特にテスト対象のサンプルコードとしての価値 が大きく下がってしまう 問題点
6. スパゲッティ・テスト 処方箋 AAAパターン または Given/When/Then
• テストコードを以下の3つのセクションに分けるプラクティス ◦ Arrange / Given - 事前条件の準備 ◦ Act
/ When - テスト対象の実行 ◦ Assert / Then - 事後条件の検査 • それぞれの役割ごとにコードのブロックを分割することで構造化し、テストコードの 可読性向上を実現 • 2つのパターンは名前や由来が異なるだけなので、どちらでもお好きな方、しっくり 来る方を採用するとよい AAAパターン・Given/When/Then パターン
#[Test] public function fetchMyTeam_APIが_200_OK_を返す場合は手元のポケモン一覧を返す(): void { $httpClientMock = Mockery::mock(HttpClient::class); $httpClientMock
->expects('get') ->with('/api/v2/my-team') ->andReturn(new Response(200, [], json_encode([ ['name' => 'プリン', 'level' => 88, /* ... */], // ... ]))); $subject = new PokemonService($httpClientMock); $expected = [ new Pokemon(name: 'プリン', level: 88, /* ... */), // ... ]; $actual = $subject->fetchMyTeam(); self::assertEquals($expected, $actual); } Before
#[Test] public function fetchMyTeam_APIが_200_OK_を返す場合は手元のポケモン一覧を返す(): void { // Arrange $httpClientMock =
Mockery::mock(HttpClient::class); $httpClientMock ->expects('get') ->with('/api/v2/my-team') ->andReturn(new Response(200, [], json_encode([/* ... */]))); $subject = new PokemonService($httpClientMock); // Act $actual = $subject->fetchMyTeam(); // Assert $expected = [/* ... */]; self::assertEquals($expected, $actual); } After
7. モック地獄
7. モック地獄 #[Test] public function update_更新されたインスタンスを返す(): void { // Arrange
$repository = Mockery::mock(PokemonRepositoryInterface::class); $repository->allows('store')->... $fooService = Mockery::mock(FooServiceInterface::class); $fooService->allows('performAction')->... $barService = Mockery::mock(BarServiceInterface::class); $barService->allows('executeTask')->... $bazService = Mockery::mock(BazServiceInterface::class); $bazService->allows('runProcess')->... // ... $useCase = new UpdatePokemonUseCase($repository, $fooService, $barService, $bazService, /* ... */); // Act // ... }
7. モック地獄 • 「単体テスト」の言葉に囚われすぎてしまった結果、あらゆる依存をモック化してしま う 症状 • テストコードがテスト対象の内部実装と密結合してしまい、リファクタリング耐性の 低い、保守性の低いテスト に
• テストコードがモックの設定で溢れかえり、可読性が著しく低下 • モックの設定ミスによって「本物」と異なる振る舞いをさせてしまうことでテストの 信頼性が低下する場合も 問題点
7. モック地獄 処方箋 モックの使用を最小限にする または テスト対象モジュールの設計見直し
モックの使用を最小限にする • ここで言う「モック」は正確には「テストダブル」のこと • 「依存はすべてモックにするべき」という思い込みを捨て 、必要な場合にだけモック (テストダブル)を使う • データベースやファイルシステムなどグローバルな状態 は引き続きモック化(テストダ
ブル化)する必要がある
テスト対象モジュールの設計見直し • テスト対象のモジュールの責務が多すぎるとテストが複雑になってしまい、依存するモ ジュールも増えてしまいがち • 「モックの使用を最小限にしたときにテストが大変」という場合は、モジュールの設計 を見直してみた方がいいかも?
8. モック恐怖症
8. モック恐怖症 • モック地獄を経て、モックによる弊害を怖れるあまりにモックの使用を過度に避けてし まい、データベースなどに依存したテストばかりを書いてしまう 症状 • データベースやファイルシステム、外部 API に依存してしまうことで速度が低下
• 外部依存の障害等によってテストが不安定化(Flaky 化) • テストが失敗した時に、コードと外部依存のどっちに問題があるのかわからないため 失敗時の原因切り分けが困難に • テスト環境のセットアップが複雑化し、手軽に実行できなくなってしまう 問題点
8. モック恐怖症 処方箋 用法・用量を守る
用法・用量を守る • テストダブルは怖くないよ! • モックではなくフェイクやスタブを使うとテストコードもシンプルに • グローバルな状態 ・外部依存は積極的にテストダブルを用いるべき ◦ データベース(MySQL,
PostgreSQL, Redis, etc...) ◦ ファイルシステム ◦ 外部 API ◦ システムクロック
おしながき 1 AI時代におけるユニットテストの価値とは 4 まとめ 2 価値のあるテストコードを書くための基本的な考え方 3 よくあるアンチパターンと処方箋・テクニック
まとめ • AI エージェントが普及した現代において自動化されたテストの価値は向上 • テストを書く上でも AI は強い味方だが、丸投げできる状況ではない • AI
を活用し安全かつ高い生産性を実現する上で、ユニットテストにおいては 高速性・信頼性・移植性・可読性 が4つが特に重要 • 先人の知恵を活用 することで、テストコードの価値を高めることができる • テストを書こう