この記事は G's ACADEMY Advent Calendarの14日目の記事です。
はじめに
初心者がTDDから始めることに対して賛否あるかと思いますが、テストに関してずっとモヤモヤしていた私にはこのアプローチがしっくりきたので、そのご紹介です。
ちなみに、そのモヤモヤについての考察はこちらの記事をご覧ください。
なぜテストが書けなかったのか?初心者がLaravelで初テストを書くまで
想定読者
この記事ではLaravelを使ったアプリケーション作成を前提として、テストに関わること以外の基礎的なところにあまり触れずに話を進めます。
- Laravelの環境があり、基本的なCRUDが実装できる
- DBのマイグレーションやシーダーなどが説明できる
上記のような最低限のLaravelのお作法が理解できている方を想定します。
その上で、
- テストを書くのに興味あるけど、何から始めればいいか分からない人
- テスト駆動開発(TDD)を試してみたい人
に読んでいただけるといいのではないかと思います。特に何から始めればいいか分からなかったのは、まさにここ数ヶ月の私のことです!
用意するもの
PHPフレームワークLaravel Webアプリケーション開発という本を使いました。私は社内の有志でやってる勉強会での課題図書だったので購入しました。
この本は、有名な青い本と比べると少しレベルが高いようです。
(実は青い本、読んでないんですが、社内のPHPerの強い人たちがこの本は初心者向きじゃないよって言ってました)
でも、ぜひ11章だけは手を動かしながらやってみて欲しい!
「11章 テスト駆動開発の実践」という章は、ここだけで完結しているので前の章をやっていなくても大丈夫な構成になっています。
私が、この本の11章でTDDをやって見て、いいなと思ったところはこんな感じです。(本の特徴というよりTDDの特徴によるところも混ざっている気がします。。。)
- テストありきで設計していくのでテストを書く過程が追える
- 小さく小さくテストを書いていくのでテスト自体が単純で書きやすい
- TDDの基本的な考え方を手を動かしながら知ることができる
- テストに関するLaravelの機能をざっくり知ることができる
- テンポよくテストが書けて気持ちいい
- 通してやるとTDD良さそう感が体験できる
- 何より、テストを書くための準備について触れてくれていた
TDDはそれだけで本一冊になるテーマなので、私が知らないだけで良書はたくさんあると思います。ただ手軽さという点でもたまたま読んだこの本は、私には取っ掛かりとしてとても良かったです。
この本の11章でできること
- 単純なアプリケーションでTDDの実践
- APIテストの実装
- テスト用のDBの用意(シーダーの実行)
- DBテストの実装(CRUDとバリデーション)
- 最低限の実装からフレームワークのお作法に近い形の実装への変換(コントローラーへのせかえ)
- コントローラーからさらにサービスクラスへの分離
詳細な説明は書籍に譲りますが、こんな感じで11章は結構やることは盛りだくさん!
なのですが、TDDのテンポに乗ってコードを書いていくと、なるほどなるほどー!!という感じでサクサクいけて、私はテスト駆動楽しいなーって思えました。
おかげで、テストに対して何から手をつけていいのやら、、、と途方に暮れる感覚、苦手意識はどっかに行ってしまいました!
これをやってみてテンションが上がったら、「9章 テスト」の章に戻って自分で勉強してみるといいかもしれません。テストについては9章の方がもう少し細かく、いろいろ書いてあります。
ということで以下では、
- TDDの進め方の概要
- テストで使った機能の一部を紹介
をまとめていきます。
基本的なTDDの進め方
本文の中では、TDDは次のような手順で進めていきます。
- アプリケーションを想定したテーブル構成とAPIの設計をする
- テストを書くまえにAPIの各エンドポイントで実行可能にするべき
ToDoリスト
を作る - テストファイルの作成
- 先に検証のためのテストコードを書く(テストは失敗する)
- 次に実行部分のテストコードを書く(テストは失敗する)
- 最低限の実装を書く
- テストが通るか確認する(テストは成功する)
1〜3で下準備をして、4からやっとテストを書き始めます。
実装は6で書くので当然4と5の2つのテストは失敗します。でも失敗することでやるべきことが明確になり、リズムが生まれるそうです。そして次のToDoに対してまた4〜7を繰り返しながらテストと実装を書いていきます。これでToDoリストを潰していくのがTDDの開発の流れです。
実はこの、2つ目のToDoリストを作るというのがポイントです。
テストが書けなくて戸惑っていた私は、まさにここをしっかり言語化できていなかったのでした。
しかもテスト書ける人にとっては当たり前のことなのか、私のググり方が悪かったのか。いろいろ調べた中では、この点にしっかり触れてくれていなかったように思います。
自分で何かを実装する時は、まさに順を追ってToDoリストを頭の中に作っていたのに、テストを書こうとした時にはそれをしていなかったのが、今となっては自分でも不思議です。
また個人的には4番目と5番目の、「検証用のテスト」と「実行部分のテスト」の違いがいまいちピンとこなかったので、本書からの引用も載せておきます。これは言葉で説明するよりも実際にコードを書いた方が分かりやすいかもしれません。
テストとはそもそもなんでしょうか。ごくごく簡単にまとめると下記の通りです。つまり、テストメソッドには最低限、結果を取得する処理の「実行」と「検証」を記述する必要があります。
- 何らかの処理が「実行」されたとき、
- 結果が期待通りのものであるかを「検証」する
具体的に考えてみます。
- あるAPIのエンドポイント(api/XXX)にGETメソッドでアクセスできる
というToDoに対してのテストを考えるとすると、
「api/XXXXにGETメソッドでアクセスする」(つまり実行された)とき、
「ステータスコード200のHTTPレスポンスが返ってくる」ことを検証する
のがテストの内容となります。「実行」と「検証」の2つのテストがありますね。
掲示板アプリを例に考える
例えば掲示板アプリを作るとして、投稿記事(Articleモデル)に対して下記のようなエンドポイントを設定するとします。
内容 | エンドポイント | メソッド |
---|---|---|
記事一覧 | api/articles | GET |
記事投稿 | api/articles | POST |
記事詳細 | api/article/[article_id] | GET |
記事編集 | api/article/[article_id] | PUT |
記事削除 | api/article/[article_id] | DELETE |
上記のエンドポイントに対してのToDoリスト
- api/articlesにGETメソッドでアクセスできる
- api/articlesにPOSTメソッドでアクセスできる
- api/article/[article_id]にGETメソッドでアクセスできる
- api/article/[article_id]にPUTメソッドでアクセスできる
- api/article/[article_id]にDELETEメソッドでアクセスできる
当たり前のことしか書いていませんが、これで良いんです。こんな感じでできるだけ小さい単位でどんどんテストを書いて作っていくのがTDDの特徴です。
当然、小さい実装を想定したテストは書きやすい!!テストに対する苦手克服にも良かったです。
ちなみに、このToDoリストは途中で変更があっても構わないようです。
作成するToDoリストは、最初から完璧なものである必要はありません。むしろ実装中に細分化したり、新たなToDoに気付きリストを追加したりすることは当然のことです。
掲示板アプリを例にした4〜7までの手順のサンプルコードは、最後にまとめて掲載しておきます。
興味のある方はTDDの流れを追ってみてください。
テストで使った機能の一部を紹介
ここからは、個人的に勉強になったなーと思うことなどを書き留めておきます。LaravelというよりはPHPの機能もありますが、まとめて書いています。
artisanコマンドでテストのファイルが作れる
みんな大好き!職人artisan
です。コマンド一発でテストの雛形を作ってくれます。相変わらず職人強し。。。
$ php artisan make:test FooTest
このコマンド一発でtests/Feature/FooTest.php
というテスト用のファイルが作成されます!テストの実行は下記のコマンドで一気にやってくれます。
$ php vendor/bin/phpunit
アノテーションを使ってシンプルにテストのメソッド名が書ける
テストのサンプルコードを見ていて、私が疑問に思っていたことの一つは、なぜメソッドが急に日本語??ということでした。
/**
* @test
*/
こういうやつをメソッドの前にあらかじめ記述しておくと、その下のコードはテストメソッドと判断されて日本語で直接メソッドを書くことができるようになります。artisanコマンドでサクッとresource作った時とかに、よく見るやつですね。私はこれ邪魔だなーと思っていつもソッコー消してました
これをアノテーションと言います。@test
と書くと、テスト用のアノテーションです。
個人的にはアノテーションと呼ぶことも知らず、これに意味があったことが驚きでしたが、知ると超便利です。テストのメソッド名もスッキリ書けていいです。
日本語を使う理由は、テストの結果を見る時に日本語の方が直感的に分かりやすい、とのことでした。また、社内の勉強会では**「テスト用に英語でちゃんと名前を考えるリソースが無駄に感じる」**という意見もありました。たしかに納得!!
一方でテストだけ日本語は気持ち悪い!という人もいたので、日本語を使うのはケースバイケースみたいです。
ちなみにアノテーションを使わない場合は、メソッド名の最初にtest
をつける必要があります。
// アノテーションを使ったテストの記述
/**
* @test
*/
public function api_articlesにGETメソッドでアクセスできる(){}
// アノテーションを使わない場合のメソッド名の例(testを頭につける)
public function test_api_articlesにGETメソッドでアクセスできる(){}
DBテストが簡単にできる
artisan
コマンドでテスト用のファイルを作ると、下記のように自動的にRefreshDatabaseトレイト
が利用できるようになっているので、use RefreshDatabase;
と記述すればテストが実行される前にテスト用のDBのマイグレーションを自動で行ってくれます。かしこい。
さらに、あらかじめシーダーを用意しておいて、setUpメソッド内で呼び出してあげれば、テストで使用するためのダミーデータも入れてくれます。setUpメソッドはテストが始まる前に実行されるので毎回同じデータが用意できますね。便利!
Laravelで書くのテストのいいところは、こうやってDBがからむテストを簡単に書けることが一番なのではないでしょうか。
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class FooTest extends TestCase
{
use RefreshDatabase; // これだけで自動でDBを初期化してくれる!
// setUpメソッドがテストの前処理をしてくれる.ここではシーダーを実行している
public function setUp(){
parent::setUp();
$this->artisan('db:seed', ['--class' => 'TestDataSeeder']);
}
豊富なassertメソッド
assert
は翻訳すると主張する、とか言い張る、とか宣言する、というような意味です。assertメソッドは値が一致するかどうかや、値の存在の有無などをチェックする時に使うテストに便利なメソッドです。
これは結構種類が多いので、最初に作ったToDoリストに合わせて使えそうなメソッドを選べばOK。
ちなみに11章で出てきたメソッドはこんな感じでした。
assertStatus() // クライアントのレスポンスが指定したコードでなければエラー
assertThat() // 後に続く条件を満たさなければエラー
assertSame() // 2つの引数が同じ型・同じ値でない場合にエラー
assertJsonCount() // 引数で渡されたJSONの中のデータの数が指定した数でなければエラー
assertDatabaseHas() // 引数で渡された抽出条件に一致するデータが指定のテーブルになかったらエラー
assertExactJson() // 引数で渡されたJSONと指定したデータと完全に一致しなかったらエラー
掲示板アプリ用のサンプルコード
最後に、TDDの簡単なサンプルとして、例に挙げた掲示板アプリのToDoに対して、実際どう書いていくかを少しだけ紹介します。
まずはテストファイルを作成
コマンド打つだけです
$ php artisan make:test ArticleTest
Test created successfully.
ファイルを編集して最初のテストを書く
前半で紹介したToDoリストの1番目、
- api/articlesにGETメソッドでアクセスできる
のところだけやっていきます
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ArticleTest extends TestCase
{
/**
* @test
*/
public function api_articlesにGETメソッドでアクセスできる()
{
// ①最初に検証部分(ステータス200が返る)を記述
$response->assertStatus(200);
}
①のテストを書いて、テストを実行すると、下記のように失敗します。
$ php vendor/bin/phpunit
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 1.51 seconds, Memory: 12.00MB
There was 1 error:
1) Tests\Feature\ArticleTest::api_customersにGETメソッドでアクセスできる
ErrorException: Undefined variable: response
/home/tdd_sample/tests/Feature/ArticleTest.php:25
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
②のテストを書き加えます。
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ArticleTest extends TestCase
{
/**
* @test
*/
public function api_articlesにGETメソッドでアクセスできる()
{
// ②次に実行部分(GETでアクセスする)のテストを記述
$response = $this->get('api/articles');
// ①最初に書いたテスト
$response->assertStatus(200);
}
この後、再度テストするとちょっとエラーの内容が変わりますが、これも失敗します。
$ php vendor/bin/phpunit
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 1.41 seconds, Memory: 12.00MB
There was 1 failure:
1) Tests\Feature\ArticleTest::api_customersにGETメソッドでアクセスできる
Expected status code 200 but received 404.
Failed asserting that false is true.
/home/tdd_sample/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:78
/home/tdd_sample/tests/Feature/ArticleTest.php:25
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
テストが失敗することが確認できたら、最低限の実装です。
今回はroutes/api.php
にルーティングを書くだけです。
<?php
use Illuminate\Http\Request;
Route::get('customers', function(){});
この状態で再度テストをすると、テストが成功します。(このOKが、気持ちいい!!)
vagrant@homestead:~/code/tdd_sample$ php vendor/bin/phpunit
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 1.61 seconds, Memory: 12.00MB
OK (1 test, 1 assertion)
こんな感じで実行と検証が成功すると、ひとつ目のToDoリストは完了です。
非常に単純化して書きましたが、これの繰り返しです。個人的には、このサイクルをサクサク回していくのが結構楽しかったです。
書籍の中では、もう少し複雑なアプリケーションを想定してテスト用のデータの用意もしながらTDDをざっくり体感できます。ルーティングも今は非常に雑な感じですが、こちらも最後にLaravelらしくリファクタリングされていきます。
ということで、TDDってめちゃくちゃ難しそうって思ってた私も本を見ながらサクサクできたので、意外と初テストにTDDアリかもっていうお話でした。