はじめに
Laravelでは、PHPUnitが標準で組み込まれているので、プロジェクトのセットアップ完了後、すぐにテストコードを書き始めることができます。この記事では、Featureテストを使用したWeb APIのテストコードの実装と、Unitテストを使用したデータベースのテストコードの実装について紹介します。
FeatureとUnitの使い分け
testsディレクトリの配下に、Featureディレクトリと、Unitディレクトリがあり、ここにテストコードを配置していきます。
FeatureにはControllerやRouting、Middlewareの機能テストを実装し、それ以外の単体テストをUnitに実装します。phpunit.xml
にこの2つのテストスイートが定義済みのため、テストを実行すると、両方のテストコードが実装されます。さらにテストスイートを分解したい場合は、phpunit.xml
を編集することで、テストスイートを増やすことができます。
今回は、図書貸し出し機能を例に、テストコードについて説明していきます。
テスト用データベースの準備
自動テストでは、テスト中に何度かデータベースをリフレッシュするため、テスト専用のデータベースを用意しておきましょう。phpunit.xml
で、<php>
タグの中に、<server>
タグを使用すると、テスト時にenvファイルの設定を上書きすることができます。
今回は、同ホスト内に別のデータベースを用意したため、 DB_DATABASE
のvalueを、テスト用のデータベースに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?xml version="1.0"encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true"> ・・・省略・・・ <php> <server name="APP_ENV"value="testing"/> ・・・省略・・・ <!-- 以下を追記 --> <server name="DB_DATABASE"value="test_db"/> </php> </phpunit> |
これで、テストを実行したときは、テスト用のデータベースが使用されるようになります。
Featureテスト
Featureテストを使用して、Web APIのテストコードを実装したいと思います。
テスト対象のコード
まずは、対象となるマイグレーション、コントローラ、ルーティングを実装します。
マイグレーション
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 | <?php useIlluminate\Database\Migrations\Migration; useIlluminate\Database\Schema\Blueprint; useIlluminate\Support\Facades\Schema; classCreateBooksTableextendsMigration { /** * Run the migrations. * * @return void */ publicfunctionup() { Schema::create('books',function(Blueprint$table){ $table->id(); $table->string('author'); $table->string('title'); $table->string('description'); $table->integer('status')->default(\App\Book::Available); $table->integer('rent_count')->default(0); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ publicfunctiondown() { Schema::dropIfExists('books'); } } |
ルーティング(api.php)
1 2 3 4 5 6 | <?php useIlluminate\Http\Request; useIlluminate\Support\Facades\Route; Route::resource('books','BookController'); |
コントローラ(BookController.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespaceApp\Http\Controllers; useApp\Book; useIlluminate\Http\Request; classBookControllerextendsController { publicfunctionindex() { returnBook::all(); } publicfunctionstore(Request$request) { $book=newBook(); $book->title=$request->title; $book->author=$request->author; $book->description=$request->description; $book->save(); return$book; } } |
モデル(Book.php)
1 2 3 4 5 6 7 8 9 10 | <?php namespaceApp; useIlluminate\Database\Eloquent\Model; classBookextendsModel { } |
テストコードの実装
まず、テストコードのファイルを生成します。
1 | $php artisan make:testBookTest |
tests/Feature
ディレクトリに、テストコードの雛形が作成されます。ここにテストコードを実装していきます。/api/books
へのリクエストのテストコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespaceTests\Feature; useIlluminate\Foundation\Testing\RefreshDatabase; useTests\TestCase; classBookTestextendsTestCase { useRefreshDatabase; publicfunctiontestFetchBook() { $response=$this->get('/api/books'); $response->assertStatus(200); } } |
$this->get()
メソッドを使用することで、第一引数のパスにリクエストすることができます。
その他にも、$this->post()
でPOSTリクエストも可能です。その場合は、第二引数で、配列でパラメータを付与します。
1 2 3 4 5 6 7 8 9 10 | publicfunctiontestCreateBook() { $response=$this->post('/api/books',[ 'title'=>'Laravel入門', 'author'=>'Lara太郎', 'description'=>'Laravel入門書です。' ]); $response->assertStatus(201); } |
テスト結果の検証
アサーションで検証することができます。
ステータスコードの検証
assertStatus()
メソッドを使用します。
1 | $response->assertStatus(200);// ステータスコードが200であること |
JSONのパラメータ判定
assertJsonFragment()
メソッドを使用すると、引数に渡したJSONが、レスポンスに含まれているか検証します。
1 2 3 4 5 | $response->assertJsonFragment([ 'title'=>'Laravel入門', 'author'=>'Lara太郎', 'description'=>'Laravel入門書です。' ]); |
JSONの完全一致を検証する場合は、assertJson()
メソッドを使用します。
テストの実行
次のコマンドでテストを実行できます。
1 | $php artisan test |
テストに成功すると、次のように出力されます。
1 2 3 4 5 6 7 8 9 10 11 | $php artisan test PASS Feature\BookTest ✓fetch book ✓create book PASS Feature\ExampleTest ✓basic test Tests: 6passed Time: 0.73s |
テストに失敗すると、失敗箇所が表示されるため、実装を修正して、再度テストを行いましょう。
Unitテスト
Unitテストを使用して、モデルの単体テストを実装したいと思います。
テスト対象の実装
先ほどのBookモデルに、本の貸し出し・返却用のメソッドを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php namespaceApp; useIlluminate\Database\Eloquent\Model; classBookextendsModel { publicconstAvailable=1; publicconstLoanedOut=2; publicfunctioncheckOut(){ $this->increment('rent_count',1); $this->status=self::LoanedOut; $this->save(); } publicfunctionreturnBook(){ $this->status=self::Available; $this->save(); } } |
Factory
Factoryは、ダミーレコードを作成できる機能です。引数にFakerが入るため、ダミーデータをランダムに生成することも可能です。
今回は、あらかじめ貸し出す本を登録しておくため、Factoryを使用して、booksテーブルにインサートしていきます。
まず、Factoryファイルを作成します。
1 | $php artisan make:factory BookFactory--model=Book |
--model
オプションで、ファクトリで生成するモデルを指定します。ここで指定したモデルが、ファクトリ内で定義されます。
このモデルクラスを指定しないと、ファクトリでデータを生成しようとしても、エラーとなるため、必ず指定しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ useApp\Book; useFaker\Generator asFaker; $factory->define(Book::class,function(Faker$faker){ return[ 'author'=>$faker->name, 'title'=>$faker->title.$faker->randomNumber().$faker->time(), 'description'=>$faker->paragraph, 'status'=>$faker->numberBetween(1,2), 'rent_count'=>$faker->randomNumber() ]; }); |
カラム名をキーとして、値を設定していきます。 $faker
を使用している箇所は、それぞれランダムな値が入ります。
Fakerの主なメソッドを紹介します。
randomNumber()
ランダムな正数値を生成します。numberBetween(min, max)
min〜maxの間のランダムな正数値を生成します。email
ランダムなメールアドレスを生成します。phoneNumber
ランダムな電話番号を生成します。name
ランダムな人物名を生成します。sentence()
ランダムな1文を生成します。paragraph()
ランダムな1段落分の文章を生成します。
そのほかにも、様々なフォーマットがあります。
参考: Faker (Github)
単体テストの実装
単体テストのファイルを作成します。 --unit
オプションを付けると、 tests/Unit
ディレクトリに作成されます。単体テストの場合は、Featureと区別を付けるために、このオプションを使用した方が良いでしょう。
1 | $php artisan make:test BookTest--unit |
テストコードを実装します。
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 43 44 45 46 47 48 49 50 51 52 53 | <?php namespaceTests\Unit; useApp\Book; useIlluminate\Foundation\Testing\RefreshDatabase; useTests\TestCase; classBookTestextendsTestCase { useRefreshDatabase; protectedfunctionsetUp():void { parent::setUp(); // BookFactoryを使用してbookを100レコード用意する for($i=0;$i<100;$i++){ factory(Book::class)->create(); } } /** * 本の貸し出しテスト */ publicfunctiontestCheckOut() { $book=Book::where('status',Book::Available)->first(); $count=$book->rent_count; $book->checkOut(); // 貸し出し回数が、元の+1になっていることを確認 $this->assertEquals($book->rent_count,$count+1); // ステータスが貸し出し中になっていることを確認 $this->assertEquals($book->status,Book::LoanedOut); } /** * 本の返却テスト */ publicfunctiontestReturnBook() { $book=Book::where('status',Book::Available)->first(); $count=$book->rent_count; $book->returnBook(); // 返却時は、貸し出しが変化していないことを確認 $this->assertEquals($book->rent_count,$count); // ステータスが利用可能になっていることを確認 $this->assertEquals($book->status,Book::Available); } } |
setUp()
メソッドは、各テストが始まる前に毎回呼ばれます。 RefreshDatabase
トレイトを使用しているため、テスト実施ごとにデータベースの全レコードが削除されるため、ここでFactoryを使用してデータを投入します。
ここでは使用していませんが、tearDown()
メソッドは、テスト終了時に毎度呼ばれるメソッドで、各テスト後の後処理に使用します。testCheckOut()
メソッドとtestReturnBook()
メソッドが、今回実装したテストです。assertEquals()
メソッドを使用して、実行結果を検証します。
テストの実行
Featureテストと同じく、次のコマンドでテストを実行できます。
1 | $php artisan test |
さいごに
いかがでしたか。Laravelでは初めからPHPUnitを使用することができるので、手軽にテストコードを書き始めることができました。適切なテストコードを書いて、システムの品質・保守性向上に繋げたいと思います。