こんにちは、かつコーチです。
前回はFeature Testで、コントローラを含めた機能全体の動作を検証する方法を解説しました。
今回は、モデルやサービスクラスといった「単体のロジック」を検証するUnit Testを解説します。
Unit Testの役割を再確認する
Feature Testとの役割分担
前回のおさらいですが、Laravelのテストは役割によって使い分けます。
- Feature Test:HTTPリクエストを送り、画面・APIとしての振る舞いを検証する
- Unit Test:クラス単体のロジックが正しいかを、外部に依存せず検証する
たとえば「割引金額の計算ロジックが正しいか」を確かめたいだけなら、わざわざHTTPリクエストを送るFeature Testを書く必要はありません。
計算ロジックを持つクラスを直接呼び出して検証する方が、テストの実行も速く、失敗した原因も特定しやすくなります。
Unit Testが向いているコード
Unit Testが特に効果を発揮するのは、次のようなコードです。
- 金額計算・割引計算などのビジネスロジック
- 文字列の整形・バリデーションルールの判定
- 外部APIを直接呼ばない、純粋な計算処理
逆に、DBへの保存やHTTPリクエストが絡む処理は、次回解説するFactoryなどを使ったテストか、Feature Testの領域になります。
サービスクラスをテストしてみる
テスト対象のサービスクラスを用意する
例として、注文金額に応じて割引額を計算するサービスクラスを用意します。
<?php
// app/Services/DiscountService.php
namespace App\Services;
class DiscountService
{
public function calculate(int $totalPrice): int
{
if ($totalPrice >= 10000) {
return (int) ($totalPrice * 0.1);
}
if ($totalPrice >= 5000) {
return (int) ($totalPrice * 0.05);
}
return 0;
}
}
1万円以上で10%引き、5千円以上で5%引き、それ未満は割引なし、というシンプルなロジックです。
Unit Testを書く
このクラスをUnit Testで検証します。
<?php
// tests/Unit/DiscountServiceTest.php
namespace Tests\Unit;
use App\Services\DiscountService;
use PHPUnit\Framework\TestCase;
class DiscountServiceTest extends TestCase
{
private DiscountService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new DiscountService();
}
public function test_1万円以上は10パーセント引きになる(): void
{
$this->assertEquals(1000, $this->service->calculate(10000));
}
public function test_5千円以上1万円未満は5パーセント引きになる(): void
{
$this->assertEquals(250, $this->service->calculate(5000));
}
public function test_5千円未満は割引なし(): void
{
$this->assertEquals(0, $this->service->calculate(4999));
}
}
setUpメソッドは、各テストの実行前に必ず呼ばれる特別なメソッドです。
テストごとに毎回new DiscountService()と書く代わりに、setUpで共通の準備をまとめておくと、テストコード自体もすっきりします。
モデルのロジックをテストする
アクセサ・ミューテータのテスト
Eloquentモデルに定義した独自ロジック、たとえばアクセサもUnit Testの対象になります。
<?php
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected function displayName(): Attribute
{
return Attribute::make(
get: fn () => $this->last_name . ' ' . $this->first_name,
);
}
}
<?php
// tests/Unit/UserTest.php
namespace Tests\Unit;
use App\Models\User;
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function test_表示名が姓名の順で結合される(): void
{
$user = new User([
'last_name' => '山田',
'first_name' => '太郎',
]);
$this->assertEquals('山田 太郎', $user->display_name);
}
}
このテストはDBへの保存を行わず、インスタンスを生成しているだけなので、Unit Testとして高速に実行できます。
モックを使って依存を切り離す
外部サービスに依存するクラスの扱い
サービスクラスの中には、外部APIを呼び出すクラスに依存しているものもあります。
そのままテストすると、実際に外部APIへリクエストが飛んでしまい、テストが不安定になったり時間がかかったりします。
そういった場合は、依存クラスをモック(偽物のオブジェクト)に差し替えてテストします。
<?php
// tests/Unit/NotificationServiceTest.php
namespace Tests\Unit;
use App\Services\NotificationService;
use App\Services\MailClient;
use PHPUnit\Framework\TestCase;
class NotificationServiceTest extends TestCase
{
public function test_通知メールの送信メソッドが呼ばれる(): void
{
$mailClientMock = $this->createMock(MailClient::class);
$mailClientMock->expects($this->once())
->method('send')
->with('user@example.com', '注文が確定しました');
$service = new NotificationService($mailClientMock);
$service->notifyOrderConfirmed('user@example.com');
}
}
createMockで作った偽物のMailClientを注入し、「sendメソッドが1回だけ、指定した引数で呼ばれたか」だけを検証しています。
実際にメールが送信されることはないため、高速かつ安定してテストできます。
つまずきやすいポイント:Unit TestでDBアクセスしてしまう
テストが遅く不安定になる原因
私が最初にUnit Testを書いていた頃、モデルの検証のつもりで、うっかりDBへの保存を伴うコードを混ぜてしまったことがあります。
❌ Before:Unit TestなのにDBへ保存してしまう
<?php
namespace Tests\Unit;
use App\Models\User;
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function test_表示名が姓名の順で結合される(): void
{
$user = User::create([
'last_name' => '山田',
'first_name' => '太郎',
'email' => 'yamada@example.com',
'password' => bcrypt('password'),
]);
$this->assertEquals('山田 太郎', $user->display_name);
}
}
User::createはDBへの書き込みを伴うため、TestCaseのままではDB接続エラーになったり、テストが極端に遅くなったりします。
✅ After:DBが必要ない検証は、インスタンス生成だけで済ませる
<?php
public function test_表示名が姓名の順で結合される(): void
{
$user = new User([
'last_name' => '山田',
'first_name' => '太郎',
]);
$this->assertEquals('山田 太郎', $user->display_name);
}
「本当にDBへの保存が必要な検証か」を都度考える習慣がつくと、Unit TestとFeature Testの使い分けが自然と身につきます。
DBを使った検証がどうしても必要な場合は、次回解説するFactoryとRefreshDatabaseの出番です。
まとめ
この記事のポイント
- Unit Testは、DBやHTTPリクエストに依存しない単体ロジックを高速に検証するテスト
setUpメソッドで、各テストに共通する準備処理をまとめられる- 外部サービスに依存するクラスは、モックに差し替えて依存を切り離す
- Unit TestでうっかりDBアクセスするコードを書くと、テストが遅く不安定になるので注意する
次に読むべき記事
次回は、DBを使ったテストを安全かつ効率的に行うための「Factory」と「RefreshDatabase」を解説します。
→ 次の記事:FactoryとRefreshDatabaseでテストデータを整える