第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%になってます。