こんにちは、バックエンドエンジニアの中嶋です。
今年の春に入社して主にLaravelを扱って開発をしていますが、
プロダクト開発はTDD(テスト駆動開発)で行うことが多くなってきました。
もちろん規模や目的によってTDDを導入すべきかは検討が必要ですが、
「テストなんて面倒だしそもそも何からやって良いかわからない」という心理的なハードルがあることで導入していないパターンも割と多いのではないでしょうか?
そこで今回は、
- Laravelを用いてまだテストコードを書いたことがない
- TDD(テスト駆動開発)を実践したことがない
- 気にはなるけど実践するのはハードルが高くて..
という方に向けてTDDの雰囲気だけでも知ってもらうべく、その手順を簡単に紹介したいと思います。
今回やること
↓もし「APIって何?」という方はこちらに簡単に纏めているので良ければご覧ください
REST APIとは?ざっくりと理解してみる【初心者向け】 - Wiz テックブログ
テスト駆動開発はその名の通りテストを中心とした開発手法のことです。
「Test-Driven Development」の頭文字を取ってTDDと呼ばれることが多く、
XP(エクストリームプログラミング)の考案者であるケント・ベック氏が編み出した手法とされています。
概要をざっくり一言でまとめると、
ちゃんと動く状態を保ちながら機能を素早く実装し、きれいなコードを目指してリファクタリングしていく
です。
そのプロセスとしては
- きちんと動作するか?のテストを作成する
- テストを実行して失敗することを確認する
- コードが汚くても良いので最低限仕様を満たす実装を行う
- テストを実行して成功するようになったことを確認する
- テストが成功することを確認しながらきれいなコードにリファクタリングしていく
というような流れになります。
コツとしては「テスト作成」「機能実装」「リファクタリング」を、それぞれ最小の単位で素早く行うことです。
テストや機能を素早く実装し、テストが常にグリーンの状態になる..このテストに守られている安心感があると、開発のリズムも良くなってくるはずです。
逆にテストなしに大きな単位で実装やリファクタリングを行うと、
「あれ?なんで動かないんだ?」「どこで間違えたんだ?」ということに時間や思考を取られるので、
テストに守られた状態になれるというだけでもかなり恩恵が得られると思います。
テストの手順
さて、概要がわかったところで「PHPUnit」を用いたテストの作成をしていきます。
PHPにおけるテストはこのPHPUnitがデファクトスタンダード(事実上の標準)と言えると思います。
そしてLaravelでもあらかじめ用意されており、そのまま使用することができます。(必要に応じて設定は必要です)
(Tips) 設定ファイルについて
Laravelのプロジェクトディレクトリ直下にphpunit.xml
というファイルが用意されています。
これはPHPUnitのテスト実行の設定ファイルで、
今回のような簡易なテストではデフォルトのまま実行することができますが、テストケースをグループ化したりテスト用のデータベースを用意する場合など必要に応じて編集します。
【手順①】テストしたいことリストを作成
まずは「どんな機能をテストしたいか」をリストアップします。
今回の例では「お知らせAPI」という新着ニュースのデータを返す簡易なAPIをテストしたいので、
- 'api/notifications' というエンドポイントにGET メソッドでアクセスできる(ステータス200を返す)
- 「お知らせ」データがjson形式で取得できる
- 取得したJSONデータがデータベースに登録されている値と一致する
..
といった項目などがリストアップできると思います。
これはテストコードの中にコメントアウトとして書く程度でもOKです。(今回はGETでアクセスできるかまで検証します)
【手順②】 テストクラスを作成
実際のテストコードを作成するにはartisanのmakeコマンドを使います。
今回の例ではNotificationApiTest
というクラスを作成します。
$ php artisan make:test NotificationApiTest
これでTests/Feature/NotificationApiTest.phpが雛形として生成されました。
(Tips) FeatureテストとUnitテストについて
このように特にオプションを付けないで実行すると、Featureテストのディレクトリにファイルが生成されます。
tests
├── CreatesApplication.php
├── Feature
├── ExampleTest.php
└── NotificationApiTest.php
├── TestCase.php
└── Unit
└── ExampleTest.php
これはTests\TestCaseクラスを継承していて、LaravelがPHPUnitを拡張したものになります。
Laravelの機能を利用したテストを行う場合はこちらを用いるのが良いと思います。
一方、上記コマンドで--unit
というオプションを付けて実行するとtests/Unitディレクトリにファイルが生成されます。
こちらはPHPUnit\Framework\TestCaseクラスを継承しており、Laravelの機能を利用しない範囲で粒度の細かいテストを行う場合はこちらを用いると良いと思います。
【手順③】 失敗するテストを書く
先に述べた手順のとおり、機能実装する前にテストを作成します。
本来はテスト用のデータベースやもっと細かいテスト項目を用意すべきですが、今回はお試しなので簡易な格好としています。
・Tests/Feature/NotificationApiTest.php
<?php
..
class NotificationApiTest extends TestCase
{
@test
public function GETで正常にアクセスできるか()
{
$response = $this->get('/api/notifications');
$response->assertStatus(200);
}
テストメソッドの内容としては、エンドポイントにGETでアクセスする擬似リクエストを作成し、
その結果をアサーションでチェックしています。
(Tips) テストメソッドの名前について
今回のテストメソッド名は日本語になっていて違和感のある方も多いと思います。
これは@test
アノーテーション(/**の部分)を設けていることで、メソッド名に日本語を用いても問題なく実行できるというものです。
@test
アノーテーションを使わない場合はメソッド名をtestFoo()
のようにする必要があるのですが、
どちらも動作は同じなので、メソッド名を毎回考えるのが面倒なのと日本語の方がテスト結果を見た時にも直感的でわかりやすいという理由で、
個人的には@test
アノーテーションを使用してメソッド名は日本語で定義することが多いです。
(Tips) assertメソッドについて
assert
メソッドは「テストが成功したか」「失敗したか」をチェックして結果を返すものです。
テストコードを書くにあたってはこのassert
メソッドをいかに知っているかが大事だったりします。
これは本当にたくさんの種類があるので具体的な紹介は公式ドキュメントに譲りたいと思いますが、
今回のようなAPIのテストにおける代表的なものを少しだけ紹介すると以下のようなものがあります。
メソッド |
テスト内容 |
assertSuccessful() |
HTTPステータスコードが200番台なら成功 |
assertStatus($status) |
HTTPステータスコードが$statusと一致していれば成功 |
assertJson(array $data, $strict=false) |
レスポンスされたjsonの配列に$dataが含まれていれば成功 |
assertExactJson(array $data) |
レスポンスされたjsonの配列が$dataと一致していれば成功 |
【手順④】 テスト実行 →失敗する
最低限のテストを作成したら、まずは実行して失敗することを確認します
vendor/bin/phpunit tests/Feature/NotificationApiTest.php
PHPUnit 8.2.5 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 1.51 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\NotificationApiTest::GETで正常にアクセスできるか
Response status code [404] does not match expected 200 status code.
Failed asserting that false is true.
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:89
/var/www/html/tests/Feature/NotificationApiTest.php:39
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
当然ですが、エンドポイントを実装していないのでステータスコード404(NotFound)が返り失敗します。
【手順⑤】 最低限の実装 →テストを成功させる
エンドポイントを定義
404が返っているので、まずはエンドポイントを定義します。
・api.php
<?php
..
Route::get('notifications', 'Api\NotificationApiController@index')->name('notifications');
もう一度テストを実行
PHPUnit 8.2.5 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 2.58 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\NotificationApiTest::GETで正常にアクセスできるか
Response status code [500] does not match expected 200 status code.
Failed asserting that false is true.
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:89
/var/www/html/tests/Feature/NotificationApiTest.php:39
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
こちらも当然ですが失敗します。
今度は404ではなく、ステータスコード500(ServerError)が返りました。
エラー内容はこれではわかりませんが、エンドポイントにはアクセスできたものの処理を実装していないのでサーバーエラーが返る状態ですね。
次にAPIの処理本体を最低限のレベルで実装します。
内容自体は今回は重要ではないので無視していただいてOKです。
・NotificationApiController.php
<?php
..
class NotificationApiController extends Controller
{
public function index()
{
$data = Notification::getNotificationsIndex();
$status = 200;
$message = 'Success';
return ResponseUtil::jsonResponse($status, $message, $data);
}
}
三度、テストを実行
この状態で改めてテストを実行します。
PHPUnit 8.2.5 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 1.97 seconds, Memory: 30.00 MB
OK (1 test, 1 assertion)
これでようやくテストグリーン(OKの状態)になりました🙌
あとはテストコードを信頼できるレベルに強化(DBも含めてデータが正しいかチェックする等)しつつ、
この繰り返しでテストグリーンを保ちながら、例えばAPI処理をtry〜catchの形にリファクタリングしたり、新たな機能を実装していくことができます。
今回は簡易な内容での導入でしたが、
実際の開発でも是非テストに守られた状態で実装していく気持ち良さを体感してみてはいかがでしょうか??
Wizでは良いものをワイワイと作るべく、エンジニアを絶賛募集しています!(ワイズと読みます)
↓↓↓興味ある方はぜひご覧ください!↓↓↓
careers.012grp.co.jp