LaravelのFormRequestのUnitテストの書き方
はじめに
株式会社ハックツ、エンジニアのみき(@take_cantik)です。⛏️
今回は、LaravelのFormRequestのUnitテストの書き方についての記事です。
よく記事などで見るテストケースに違和感を覚え、今回自分なりにやり方を考えたのでここにまとめようと思います。
結論
仕様単位でテストケースを作成しよう!
前提
この記事では、下記の仕様の「ユーザー更新のリクエスト」のバリデーションテストを扱います。
- 氏名(name): 必須、2文字以上、32文字未満
- 表示名(displayName): 任意項目、32文字未満
よく見るFormRequestのテスト
LaravelのFormRequest(バリデーション)のテストのやり方について調べると、
以下のように、チェックを行うメソッドを作成し、データプロバイダーでチェックしたいデータを羅列し、テストを行っていることが多いです。
似たようパターンとして、成功と失敗でデータプロバイダーを分け、成功パターンのテストと失敗パターンのテストで行っているのも見ますね。
コード例
<?php
/** @noinspection NonAsciiCharacters */
namespace Tests\Unit\Request;
use App\Http\Request\UpdateUserRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class UpdateUserRequestTest extends TestCase
{
/**
* @return void
*/
public function setUp(): void
{
parent::setUp();
}
/**
* @param bool $expected
* @param array $data
* @return void
*/
#[Test]
#[DataProvider('validationProvider')]
public function testValidation(bool $expected, array $data): void
{
$this->assertSame($expected, $this->validate($data));
}
/**
* @param array $data
* @return bool
*/
protected function validate(array $data): bool
{
$this->app->resolving(UpdateUserRequest::class, function($resolved) use ($data) {
$resolved->merge($data);
});
try {
app(UpdateUserRequest::class);
return true;
} catch (HttpResponseException) {
return false;
}
}
/**
* @return array
*/
public static function validationProvider(): array
{
return [
'【OK】氏名が2文字' => [
true,
[
'name' => str_repeat('あ', 2),
'displayName' => 'テスト表示名',
]
],
'【OK】氏名が31文字' => [
true,
[
'name' => str_repeat('あ', 31),
'displayName' => 'テスト表示名',
]
],
'【OK】表示名が存在しない' => [
true,
[
'name' => 'テスト太郎',
]
],
'【OK】表示名が31文字' => [
true,
[
'name' => 'テスト太郎',
'displayName' => str_repeat('あ', 31),
]
],
'【NG】氏名が存在しない' => [
false,
[
'displayName' => 'テスト表示名',
]
],
'【NG】氏名が1文字' => [
false,
[
'name' => 'あ',
'displayName' => 'テスト表示名',
]
],
'【NG】氏名が32文字' => [
false,
[
'name' => str_repeat('あ', 32),
'displayName' => 'テスト表示名',
]
],
'【NG】表示名が32文字' => [
false,
[
'name' => 'テスト太郎',
'displayName' => str_repeat('あ', 32),
]
],
];
}
}
テスト結果は以下のような感じ
【PASS】Tests\Unit\Request\UpdateUserRequestTest
✓ validation with data set "【ok】氏名が2文字" 0.18s
✓ validation with data set "【ok】氏名が31文字" 0.02s
✓ validation with data set "【ok】表示名が存在しない" 0.02s
✓ validation with data set "【ok】表示名が31文字" 0.02s
✓ validation with data set "【ng】氏名が存在しない" 0.03s
✓ validation with data set "【ng】氏名が1文字" 0.02s
✓ validation with data set "【ng】氏名が32文字" 0.06s
✓ validation with data set "【ng】表示名が32文字" 0.03s
Tests: 8 passed (8 assertions)
Duration: 0.46s
このやり方だと、正直分かりにくいなと思います。
例えば、今回で言う「氏名が2文字でOK」はそれ単体のテストを見た時に、そのテストが通ったとして何が担保したいの?となります。
また、1つの仕様が変わった際に修正するテストケースが複数になってしまい、何を修正すべきかはパッと見分かりづらいと感じます。
仕様ベースのテストケース
ということで、上記らの問題を解決するために、仕様ごとにテストケースを作成してみます。
今回は、以下のような仕様です。
- 氏名(name): 必須、2文字以上、32文字未満
- 表示名(displayName): 任意項目、32文字未満
ここから、以下のようにテストケースを考えます。
- 氏名が必須であること
- 氏名が2文字以上であること
- 氏名が32文字未満であること
- 表示名が任意項目であること
- 表示名が32文字未満であること
これをテストコードに落とし込んでみます。
<?php
/** @noinspection NonAsciiCharacters */
namespace Tests\Unit\Request;
use App\Http\Request\UpdateUserRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class UpdateUserRequestTest extends TestCase
{
/**
* @return void
*/
public function setUp(): void
{
parent::setUp();
}
/**
* @return void
*/
#[Test]
public function 氏名が必須であること(): void
{
$dataWithName = [
'name' => 'テスト太郎',
'displayName' => 'テスト表示名',
];
$actualWithName = $this->validate($dataWithName);
$this->assertTrue($actualWithName);
$dataWithoutName = [
'displayName' => 'テスト表示名',
];
$actualWithoutName = $this->validate($dataWithoutName);
$this->assertFalse($actualWithoutName);
}
/**
* @return void
*/
#[Test]
public function 氏名が2文字以上であること(): void
{
$dataWith2Char = [
'name' => str_repeat('あ', 2),
'displayName' => 'テスト表示名',
];
$actualWith2Char = $this->validate($dataWith2Char);
$this->assertTrue($actualWith2Char);
$dataWith1Char = [
'name' => str_repeat('あ', 1),
'displayName' => 'テスト表示名',
];
$actualWith1Char = $this->validate($dataWith1Char);
$this->assertFalse($actualWith1Char);
}
/**
* @return void
*/
#[Test]
public function 氏名が32文字未満であること(): void
{
$dataWith31Char = [
'name' => str_repeat('あ', 31),
'displayName' => 'テスト表示名',
];
$actualWith31Char = $this->validate($dataWith31Char);
$this->assertTrue($actualWith31Char);
$dataWith32Char = [
'name' => str_repeat('あ', 32),
'displayName' => 'テスト表示名',
];
$actualWith32Char = $this->validate($dataWith32Char);
$this->assertFalse($actualWith32Char);
}
/**
* @return void
*/
#[Test]
public function 表示名が任意項目であること(): void
{
$dataWithDisplayName = [
'name' => 'テスト太郎',
'displayName' => 'テスト表示名',
];
$actualWithDisplayName = $this->validate($dataWithDisplayName);
$this->assertTrue($actualWithDisplayName);
$dataWithoutDisplayName = [
'name' => 'テスト太郎',
];
$actualWithoutDisplayName = $this->validate($dataWithoutDisplayName);
$this->assertTrue($actualWithoutDisplayName);
}
/**
* @return void
*/
#[Test]
public function 表示名が32文字未満であること(): void
{
$dataWith31Char = [
'name' => 'テスト太郎',
'displayName' => str_repeat('あ', 31),
];
$actualWith31Char = $this->validate($dataWith31Char);
$this->assertTrue($actualWith31Char);
$dataWith32Char = [
'name' => 'テスト太郎',
'displayName' => str_repeat('あ', 32),
];
$actualWith32Char = $this->validate($dataWith32Char);
$this->assertFalse($actualWith32Char);
}
/**
* @param array $data
* @return bool
*/
protected function validate(array $data): bool
{
$this->app->resolving(UpdateUserRequest::class, function($resolved) use ($data) {
// キャッシュが残るため、mergeではなくreplaceを使用
$resolved->replace($data);
});
try {
app(UpdateUserRequest::class);
return true;
} catch (HttpResponseException) {
return false;
}
}
}
テスト結果は以下の通り
【PASS】 Tests\Unit\Request\UpdateUserRequestTest
✓ 氏名が必須であること 0.19s
✓ 氏名が2文字以上であること 0.02s
✓ 氏名が32文字未満であること 0.03s
✓ 表示名が任意項目であること 0.02s
✓ 表示名が32文字未満であること 0.02s
Tests: 5 passed (10 assertions)
Duration: 0.36s
こちらの方が圧倒的に分かりやすいですね。
各テストが何を担保しているか明確ですし、何か1つ仕様が変わった際にも変えるべき箇所が該当するいずれか1つのテストケースとなるので、変更もやりやすいです。
このように、仕様単位でテストケースを作成することによって、多くの恩恵を受けることができます。
さいごに
今回は、LaravelのFormRequestのUnitテストの書き方について書きました。
この考え方については、FormRequestだけではなくて、どのテストでも有用だと思っているので、ぜひ参考にしてみてください。
では、また別の記事で👾
Discussion