
TDD入門してみた by Laravel
2023.08.07
今回はTDDについて学習したので、そのまとめになります。
学習といっても、TDDという開発手法は奥が深く、またそれを使いこなすのはそれ何に難しいということで、何となくの雰囲気を掴んでやってみることを目標にしています。
すごく単純化したケースを題材にしているので、とっかかりになれば幸いです。
TDDとは
Test-Driven Developmentの略で、日本語ではテスト駆動開発という開発手法。
すごく雑に表現すると、テストを先に書き、そのテストが通るように実装し、そして内容を修正して綺麗にしていくやり方になります。
TDDの基本的な考え方
2つの原則
- 自動化されたテストが失敗した場合だけ、新しいコードを書く。
- 重複を除去する。
テストが先行するので、(初回は)落ちる前提のテストを作って、それを通る実装をするのが基本的な流れになります。
重複を削除というのは、例えばテストを通すために書いたコードで、共通化できるところは共通化していく、そんなイメージです。
また、TDDでの開発サイクルは大きく「レッド、グリーン、リファクタリング」の3つで構成されています。
レッド
動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く
KentBeck. テスト駆動開発 (Japanese Edition) (p.6). Kindle 版.
グリーン
そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
KentBeck. テスト駆動開発 (Japanese Edition) (p.6). Kindle 版.
※ここでの「罪」とは、良い設計とされる様々な原則を無視したコードを書くこと。
「普通ならそう書かないでしょ...」なコードも、このステップでは一時的に許されます。というのも、次のステップでリファクタが待っているためです。
リファクタリング
テストを通すために発生した重複をすべて除去する
KentBeck. テスト駆動開発 (Japanese Edition) (p.7). Kindle 版.
TDDで機能を作ってみよう
概要を抑えたので、早速TDDを意識して機能を作っていきましょう。
ゴール
今回は、メモアプリのメモ登録機能APIを実装する所に焦点を当てます。
登録に成功した場合と失敗した場合、それぞれでJSON形式のレスポンスを返します。
1 2 3 4 5 6 7 8 | // 登録成功時 { 'message' => '登録成功' } // 失敗時 { 'message' => '登録失敗' } |
準備
作業を行うにあたり、以下を事前に準備しておく必要があります。
- マイグレーションファイル作成
- モデル作成
- コントローラ作成
- ルーティング
- テストクラス作成
TDDで実装してみる
TDDでは、やるべきことをToDoリストに書き出しておきます。
その各ToDoをクリアしていくことで、気づいたら機能ができていたという流れになっています。
本当はもっと細かい粒度でやった方が良いのかもですが、今回は以下をToDoとして設定します。
- JSONレスポンスを返す
- DBへの登録処理ができる
1. JSONレスポンスを返す
ここでは、postリクエストを投げたあと、コントローラが最後に返すJSONレスポンスを実装します。
1. レッド:落ちるテストを書く
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class StoreActionTest extends TestCase { /** * A basic feature test example. */ public function testStoreSuccess(): void { $requestData = [ 'title' => 'タイトル', 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $this->post($url, $requestData) ->assertJson( [ 'message' => '登録完了', ] ) ; } public function testStoreFailed(): void { // 失敗条件 $requestData = [ 'title' => '', // とりあえずタイトルを空白に 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $this->post($url, $requestData) ->assertJson( [ 'message' => '登録失敗', ] ) ; } } |
このテストを実行するとこうなります。
1 2 3 4 5 6 7 8 9 10 | App\Http\Controllers\Api\Memo\MemoController::store(): Return value must be of type Illuminate\Http\JsonResponse, none returned --- Expected +++ Actual @@ @@ array ( - 'message' => '登録失敗', + 'message' => 'App\\Http\\Controllers\\Api\\Memo\\MemoController::store(): Return value must be of type Illuminate\\Http\\JsonResponse, none returned', 'exception' => 'TypeError', 'file' => '/var/www/app/Http/Controllers/Api/Memo/MemoController.php', 'line' => 35, |
JsonResponse型を返さないといけないのに、何も返ってないとい怒られました。
当然です。現時点ではコントローラには何も実装されていません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class MemoController extends Controller { /** * Handle the incoming request. * * @param \Illuminate\Http\Request $request * * @return \Illuminate\Http\JsonResponse; */ public function store(Request $request): JsonResponse { // まだ何も実装していない } } |
1回目のレッドをここで行いました。
2. グリーン:登録成功or失敗時のレスポンスが返るように、仮実装する
1で出たエラーをもとに、コントローラを実装しましょう。
これが上で述べたグリーンの部分になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class MemoController extends Controller { /** * Handle the incoming request. * * @param \Illuminate\Http\Request $request * * @return \Illuminate\Http\JsonResponse; */ public function store(Request $request): JsonResponse { $title = $request->get('title'); $content = $request->get('content'); if ($title === null || $content === null) { return response()->json([ 'message' => '登録失敗', ]); } // メモの作成処理を実装 // ここでは、まだDBに接続しないため、処理を省略しています return response()->json([ 'message' => '登録完了', ]); } } |
色々言いたくなるコードですが、気にせずテストを実行してみます。
※↓テストはここでは修正していません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class StoreActionTest extends TestCase { /** * A basic feature test example. */ public function testStoreSuccess(): void { $requestData = [ 'title' => 'タイトル', 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $this->post($url, $requestData) ->assertJson( [ 'message' => '登録完了', ] ) ; } public function testStoreFailed(): void { // 失敗条件 $requestData = [ 'title' => '', // とりあえずタイトルを空白に 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $this->post($url, $requestData) ->assertJson( [ 'message' => '登録失敗', ] ) ; } } |
2つともテストが通りました。
1 | OK (2 tests, 2 assertions) |
3. リファクタ:FormRequestクラスにバリデーション処理の移譲
グリーンをクリアしたら次はリファクタです。
TDDにおけるリファクタは、「テストがグリーンである範囲での修正」になります。
まずフォームリクエストクラスを作成し、以下のように実装します。
※内容的に、FormRequestの単体テストでやることかもですが...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Exceptions\HttpResponseException; class MemoRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, mixed> */ public function rules() { return [ 'title' => 'required', 'content' => 'required', ]; } public function failedValidation(Validator $validator) { $response = ['message' => '登録失敗']; throw new HttpResponseException(response()->json($response, 422)); } } |
合わせてコントローラも修正します。
1 2 3 4 5 6 | public function store(MemoRequest $request): JsonResponse { return response()->json([ 'message' => '登録完了', ]); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 修正前 public function store(Request $request): JsonResponse { $title = $request->get('title'); $content = $request->get('content'); if ($title === null || $content === null) { return response()->json([ 'message' => '登録失敗', ]); } return response()->json([ 'message' => '登録完了', ]); } |
かなりスッキリしたと思います。
2. DBへの登録処理をテスト
同じ要領でメモの登録処理を実装していきます。
1. 落ちるテストを書く
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class StoreActionTest extends TestCase { use RefreshDatabase; /** * A basic feature test example. */ public function testStoreSuccess(): void { $requestData = [ 'title' => 'タイトル', 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $response = $this->post($url, $requestData); $response->assertJson(['message' => '登録完了']); // DBへの登録ができている $this->assertDatabaseHas('memos', [ 'title' => 'タイトル', 'content' => 'メモ内容', ]); } public function testStoreFailed(): void { // 失敗条件 $requestData = [ 'title' => '', // とりあえずタイトルを空白に 'content' => 'メモ内容', ]; $url = route('api.memo.store'); $response = $this->post($url, $requestData); $response->assertJson(['message' => '登録失敗']); } } |
testStoreSuccess()にて、memosテーブルに登録されているデータを検証しています。
このテストを実行すると以下になります。
1 2 3 4 5 6 7 8 9 10 | There was 1 failure: 1) Tests\Feature\Api\Memo\StoreActionTest::testStoreSuccess Failed asserting that a row in the table [memos] matches the attributes { "title": "タイトル", "content": "メモ内容" }. FAILURES! Tests: 2, Assertions: 3, Failures: 1. |
保存処理を実装していないので、当然の結果になります。
2. DB登録されるように、仮実装する
テストが通るようにコントローラを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class MemoController extends Controller { /** * Handle the incoming request. * * @param \Illuminate\Http\Request $request * * @return \Illuminate\Http\JsonResponse; */ public function store(Request $request): JsonResponse { $validated = $request->validated(); Memo::create([ 'title' => $validated['title'], 'content' => $validated['content'], ]); return response()->json([ 'message' => '登録完了', ]); } |
テストを実行します。
1 | OK (2 tests, 3 assertions) |
無事通りました。
リファクタですが、やるとするならモデルを外に出すとかですかね...。
ケースが単純ゆえコードもシンプルなので、今回はスキップします。
感想
今回実装したのは簡単な処理でしたが、TDDでやろうとすると結構時間が掛かりました。これはシンプルに流れに慣れていないことが主な要因だと思うので、練習しようと思います。
また、テストを意識して実装する(というか強制的にそうなる)のが面白かったです。
自分で問題作ってそれを解いている感覚になりました。
1つ気になったことは、単純な機能ならTDDでやるイメージが湧くけど、大きな機能、フロントも絡む実装になってくると大変そうだなということです。
実際にTDDでガッツリ開発しているプロジェクトのお話も聞いてみたくなりました。
テストは割りと好きなので、TDDで開発できるようになったら楽しそうだなと思いました。
書いた人はこんな人

- 2022年7月に入社しました。開発未経験で未知の領域だらけですが、楽しく学びつつ、早く戦力になれるようにがんばります!
IT技術11月 30, 2023APIの認証について学んで実装してみた(by rails API)
IT技術8月 4, 2023TDD入門してみた by Laravel
IT技術6月 12, 2023型に詳しくなりたいのでPHPの勉強会に参加してみた
IT技術12月 27, 2022Homebrewで過去バージョンのパッケージをインストールする手順