第15回 Laravel10 環境構築メモ(テストコードを書いて動かしてみる)

はじめに

前回、レポジトリパターンを採用してみたのですが、今回が本番です。テストコードを書いてみたいと思います。

準備する

テストコードを書く前にいくつか設定を追加する必要がありました。coverageを測定しないなら、そのままでも動きそう。まず初めにphp.iniを修正します。第1回の時に作ったやつですね。こんな感じに修正します。

  • docker/php/php.ini

[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
default_charset = "UTF-8"
mbstring.language = "Japanese"

[xdebug]
zend_extension=xdebug
xdebug.mode=debug,coverage # ここを修正
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.start_with_request = trigger
# xdebug.start_with_request=yes
xdebug.idekey = PHPSTORM

次に.envファイルに以下の1行を追加してください。(.envを修正しないと上手くカバレッジが表示されません。)

  • .env

XDEBUG_MODE=debug,coverage #この行を追加

準備は以上です。

テストを作成

次にテストコードを書くのですが、今回はControllerのテストコードを書くため、下記のコマンドを実行してテストを作成します。

php artisan make:test GreetingControllerTest

そして、以下の通りテストコードを実装します。最初にcopilotに作成してもらったんですが、テストケースが一つ足りなかった以外は、必要なケースは作成されました。(最後に例外ケースを追加)
中身については、一部期待と違う感じの実装になってたので修正しましたが、7割くらいはcopilotが生成してくれたものを使ってます。

  • tests/Feature/GreetingControllerTest.php

<?php
namespace Tests\Feature;

use App\Repositories\Greeting\GreetingRepository;
use App\Repositories\Greeting\GreetingRepositoryInterface;
use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
use Mockery;

/**
 * Class GreetingControllerTest
 *
 * This class contains tests for the GreetingController.
 * It uses the RefreshDatabase trait to reset the database after each test.
 * It also mocks the GreetingRepositoryInterface to simulate interactions with the data layer.
 */
class GreetingControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Set up the test.
     *
     * This method is called before each test. It sets up the testing environment for each test.
     * It calls the parent's setUp method to ensure any setup in the parent class is also done.
     * It also creates a mock of the GreetingRepository class and assigns it to the $greetingRepository property.
     * This allows us to simulate interactions with the data layer in our tests.
     */
    protected function setUp(): void
    {
        parent::setUp();

        // Mock the GreetingRepository
        $this->greetingRepository = Mockery::mock(GreetingRepository::class);
    }

    /**
     * @var GreetingRepositoryInterface
     *
     * This property holds an instance of GreetingRepositoryInterface.
     * It is used to mock interactions with the data layer of the application, specifically with Greeting resources.
     */
    protected GreetingRepositoryInterface $greetingRepository;

    /**
     * Test that the index page of greetings is displayed correctly.
     */
    public function test_displays_greetings_index_page(): void
    {
        $response = $this->get(route('greetings.index'));

        $response->assertStatus(200);
        $response->assertInertia(fn(Assert $page) => $page
            ->component('Greeting/index'));
    }

    /**
     * Test that the create page of greetings is displayed correctly.
     */
    public function test_displays_greetings_create_page(): void
    {
        $response = $this->get(route('greetings.create'));

        $response->assertStatus(200);
        $response->assertInertia(fn(Assert $page) => $page
            ->component('Greeting/create'));
    }

    /**
     * Test that a new greeting is stored and the user is redirected.
     */
    public function test_stores_new_greeting_and_redirects(): void
    {
        $greetingData = [
            'country' => 'USA',
            'message' => 'Hello',
        ];

        $response = $this->post(route('greetings.store'), $greetingData);
        $response->assertRedirect(route('greetings.index'));

        $this->assertDatabaseHas('greetings', $greetingData);
    }

    /**
     * Test that an invalid greeting is not stored.
     */
    public function test_does_not_store_invalid_greeting(): void
    {
        $greetingData = [
            'country' => '',
            'message' => '',
        ];

        $response = $this->post(route('greetings.store'), $greetingData);

        $response->assertSessionHasErrors(['country', 'message']);
        $this->assertDatabaseMissing('greetings', $greetingData);
    }

    /**
     * Test that an exception is caught when storing a greeting.
     */
    public function test_does_not_store_catch_exception(): void
    {
        $greetingData = [
            'country' => 'USA',
            'message' => 'Hello',
        ];

        // Simulate an exception when the create method is called
        $this->greetingRepository->shouldReceive('create')->andthrow(new Exception());

        // Replace the instance of GreetingRepositoryInterface in the service container with the mock
        $this->app->instance(GreetingRepositoryInterface::class, $this->greetingRepository);

        $response = $this->post(route('greetings.store'), $greetingData);

        $response->assertSessionHas('flash', 'Something went wrong');
        $this->assertDatabaseMissing('greetings', $greetingData);
    }
}

なお、今回のポイントは、この↓setUpとtest_does_not_store_catch_exceptionのテストケースです。カバレッジを100%にしたかったので、例外が起こせなかったので、GreetingRepositoryをMockに差し替えて、例外をthrowするようにしています。MockにはMockeryというLaravelに標準で入ってるパッケージを使っています。
これをやりたくて、前回、レポジトリパターンを採用するに至りました。GreetingRepositoryをMockにして、 $this->app->instanceでそのMockをコンテナに突っ込んでる感じです。

    /** 省略 **/ 

    /**
     * Set up the test.
     *
     * This method is called before each test. It sets up the testing environment for each test.
     * It calls the parent's setUp method to ensure any setup in the parent class is also done.
     * It also creates a mock of the GreetingRepository class and assigns it to the $greetingRepository property.
     * This allows us to simulate interactions with the data layer in our tests.
     */
    protected function setUp(): void
    {
        parent::setUp();

        // Mock the GreetingRepository
        $this->greetingRepository = Mockery::mock(GreetingRepository::class);
    }


    /** 省略 **/ 

    /**
     * Test that an exception is caught when storing a greeting.
     */
    public function test_does_not_store_catch_exception(): void
    {
        $greetingData = [
            'country' => 'USA',
            'message' => 'Hello',
        ];

        // Mock the GreetingRepository
        $this->greetingRepository = Mockery::mock(GreetingRepository::class);

        // Simulate an exception when the create method is called
        $this->greetingRepository->shouldReceive('create')->andthrow(new Exception());

        // Replace the instance of GreetingRepositoryInterface in the service container with the mock
        $this->app->instance(GreetingRepositoryInterface::class, $this->greetingRepository);

        $response = $this->post(route('greetings.store'), $greetingData);

        $response->assertSessionHas('flash', 'Something went wrong');
        $this->assertDatabaseMissing('greetings', $greetingData);
    }

    /** 省略 **/ 

↓の前回修正したGreetingControllerのstoreのcatchのブロックをテストで通したかっただけなんですが、色々と学びがありました。

  • app/Http/Controllers/GreetingController.php

    public function store(StoreRequest $request)
    {
        try {
            $greeting = $request->only(['country', 'message',]);
            $this->greetingRepository->create($greeting);
            return to_route('greetings.index')->with('flash', 'Greeting created successfully');
        } catch (\Exception $e) {
            return back()->with('flash', 'Something went wrong'); // ここを通したい
        }
    }

テストを実行

下記のコマンドで、テストを実行してみます。

php artisan test --coverage

結果がターミナルに出力されます(上記のコマンドだとテストが全部実行されちゃいますが)こんな感じでPASSしたぞ!と表示されます。

テスト結果

小さいですが最後にカバレッジも出力され、100%になってます。

カバレッジ

この記事が気に入ったらサポートをしてみませんか?