はじめに
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 use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateBooksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { 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 */ public function down() { Schema::dropIfExists('books'); } } |
ルーティング(api.php)
1 2 3 4 5 6 | <?php use Illuminate\Http\Request; use Illuminate\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 namespace App\Http\Controllers; use App\Book; use Illuminate\Http\Request; class BookController extends Controller { public function index() { return Book::all(); } public function store(Request $request) { $book = new Book(); $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 namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { } |
テストコードの実装
まず、テストコードのファイルを生成します。
1 | $ php artisan make:test BookTest |
tests/Feature
ディレクトリに、テストコードの雛形が作成されます。ここにテストコードを実装していきます。
/api/books
へのリクエストのテストコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class BookTest extends TestCase { use RefreshDatabase; public function testFetchBook() { $response = $this->get('/api/books'); $response->assertStatus(200); } } |
$this->get()
メソッドを使用することで、第一引数のパスにリクエストすることができます。
その他にも、
$this->post()
でPOSTリクエストも可能です。その場合は、第二引数で、配列でパラメータを付与します。
1 2 3 4 5 6 7 8 9 10 | public function testCreateBook() { $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: 6 passed 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 namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { public const Available = 1; public const LoanedOut = 2; public function checkOut() { $this->increment('rent_count', 1); $this->status = self::LoanedOut; $this->save(); } public function returnBook() { $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 */ use App\Book; use Faker\Generator as Faker; $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 namespace Tests\Unit; use App\Book; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class BookTest extends TestCase { use RefreshDatabase; protected function setUp(): void { parent::setUp(); // BookFactoryを使用してbookを100レコード用意する for ($i = 0; $i < 100; $i++) { factory(Book::class)->create(); } } /** * 本の貸し出しテスト */ public function testCheckOut() { $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); } /** * 本の返却テスト */ public function testReturnBook() { $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を使用することができるので、手軽にテストコードを書き始めることができました。適切なテストコードを書いて、システムの品質・保守性向上に繋げたいと思います。