カテゴリー: BackEnd

Laravelでテストコードを書くには? Featureテスト/Unitテスト

はじめに

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を、テスト用のデータベースに変更します。

<?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のテストコードを実装したいと思います。

テスト対象のコード

まずは、対象となるマイグレーション、コントローラ、ルーティングを実装します。

マイグレーション

<?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)

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::resource('books', 'BookController');

コントローラ(BookController.php)

<?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)

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{

}

テストコードの実装

まず、テストコードのファイルを生成します。

$ php artisan make:test BookTest

tests/Featureディレクトリに、テストコードの雛形が作成されます。ここにテストコードを実装していきます。
/api/booksへのリクエストのテストコードです。

<?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リクエストも可能です。その場合は、第二引数で、配列でパラメータを付与します。

    public function testCreateBook()
    {
        $response = $this->post('/api/books', [
            'title' => 'Laravel入門',
            'author' => 'Lara太郎',
            'description' => 'Laravel入門書です。'
        ]);

        $response->assertStatus(201);
    }

テスト結果の検証

アサーションで検証することができます。

ステータスコードの検証

assertStatus()メソッドを使用します。

$response->assertStatus(200); // ステータスコードが200であること

JSONのパラメータ判定

assertJsonFragment()メソッドを使用すると、引数に渡したJSONが、レスポンスに含まれているか検証します。

$response->assertJsonFragment([
    'title' => 'Laravel入門',
    'author' => 'Lara太郎',
    'description' => 'Laravel入門書です。'
]);

JSONの完全一致を検証する場合は、assertJson()メソッドを使用します。

テストの実行

次のコマンドでテストを実行できます。

$ php artisan test

テストに成功すると、次のように出力されます。

$ php artisan test

   PASS  Feature\BookTest
  ✓ fetch book
  ✓ create book

   PASS  Feature\ExampleTest
  ✓ basic test

  Tests:  6 passed
  Time:   0.73s

テストに失敗すると、失敗箇所が表示されるため、実装を修正して、再度テストを行いましょう。

Unitテスト

Unitテストを使用して、モデルの単体テストを実装したいと思います。

テスト対象の実装

先ほどのBookモデルに、本の貸し出し・返却用のメソッドを追加します。

<?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ファイルを作成します。

$ php artisan make:factory BookFactory --model=Book

--modelオプションで、ファクトリで生成するモデルを指定します。ここで指定したモデルが、ファクトリ内で定義されます。
このモデルクラスを指定しないと、ファクトリでデータを生成しようとしても、エラーとなるため、必ず指定しましょう。

<?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と区別を付けるために、このオプションを使用した方が良いでしょう。

$ php artisan make:test BookTest --unit

テストコードを実装します。

<?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テストと同じく、次のコマンドでテストを実行できます。

$ php artisan test

さいごに

いかがでしたか。Laravelでは初めからPHPUnitを使用することができるので、手軽にテストコードを書き始めることができました。適切なテストコードを書いて、システムの品質・保守性向上に繋げたいと思います。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー
タグ: phplaravel

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前