はじめに
今回から第2部、Laravel編に入ります。第1部では、テストの仕組みを素のPHPで一つひとつ手作りしてきました。DBの準備と後片付け、テストデータを楽に作るヘルパー、依存性注入などです。
Laravelは、こうした仕組みをフレームワークの機能として最初から用意しています。今回のゴールは、第1部で学んだ考え方が、Laravelのどの機能に対応するのかを理解し、LaravelアプリのAPIをテストできるようになることです。
この回を読み終えると、次のことができるようになります。
- Laravelの
tests/Unitとtests/Featureの違いを説明できる - HTTPテストで、APIにリクエストを送ってレスポンスを検証できる
RefreshDatabaseで、テストごとにDBをきれいな状態にできる- Factoryで、テストデータを手軽に用意できる
動作確認環境
| ソフトウェア | 動作確認バージョン | 備考 |
|---|---|---|
| PHP | 8.4.26 | 第1部と同じ |
| Laravel | 13.33.0 | 2026年9月時点の最新版 |
| PHPUnit | 12.5.36 | Laravelの初期設定のバージョン |
第1部ではPHPUnit 13を使いましたが、Laravel 13の新規プロジェクトには、初期設定でPHPUnit 12が入ります。Laravel本体はPHPUnit 13にも対応していますが、この回ではLaravelの初期設定どおりに進めます。今回扱う範囲では、12と13で書き方の違いはありません。
記事中の実行結果は、すべてこの環境で実際に動かして取得したものです。
Laravelプロジェクトを用意する
プロジェクトの作成
第1部の php-test-cart とは別に、Laravelの新しいプロジェクトを作ります。名前は shop としました。
composer create-project laravel/laravel shop
cd shop
作成の途中で、アプリケーションキーの生成、SQLiteのDBファイル(database/database.sqlite)の作成、マイグレーションまで自動で行われます。バージョンを確認しておきましょう。
php artisan --version
Laravel Framework 13.33.0
【スクショ:composer create-project の完了後、php artisan –version で Laravel 13 が表示された状態】
最初からテストが用意されている
新しいプロジェクトには、サンプルのテストが2つ入っています。実行してみましょう。Laravelでは、vendor/bin/phpunit の代わりに php artisan test で実行できます。
php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response 0.12s
Tests: 2 passed (2 assertions)
Duration: 0.21s
第1部で見てきた ... の表示とは見た目が違いますが、中で動いているのは同じPHPUnitです。php artisan test は、テスト名の一覧や失敗箇所のコードを見やすく表示してくれる、Laravel向けの実行コマンドです。vendor/bin/phpunit でも同じテストを実行できます。
サンプルの
tests/Unit/ExampleTest.phpとtests/Feature/ExampleTest.phpは、中身のないテストです。この回では、自分のテストを追加した後に削除しています。
phpunit.xml に、テスト用の設定がそろっている
プロジェクト直下の phpunit.xml を開いてみてください。第2回で自分で書いたものと比べると、<php> という部分が増えています。抜粋すると次のとおりです。
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
これは、テストを実行するときだけ環境変数を上書きする設定です。主なものの意味は次のとおりです。
| 設定 | 意味 |
|---|---|
DB_CONNECTION=sqlite、DB_DATABASE=:memory: | テストではSQLiteのインメモリDBを使う(第6回と同じ考え方) |
MAIL_MAILER=array | メールを実際には送らず、配列に溜めておく |
CACHE_STORE=array、SESSION_DRIVER=array | キャッシュやセッションをメモリ上だけで扱う |
BCRYPT_ROUNDS=4 | パスワードのハッシュ計算を軽くして、テストを速くする |
第6回で手作りした「テストではインメモリDBを使う」という工夫が、Laravelでは最初から設定されているわけです。開発用の .env でMySQLを使っていても、テストはこの設定でインメモリDBに切り替わります。
Unit と Feature の違い
Laravelのテストは、tests/Unit と tests/Feature の2つのフォルダに分かれています。違いは、テストクラスが何を継承しているかです。
| フォルダ | 継承するクラス | Laravelアプリの起動 | 向いているテスト |
|---|---|---|---|
tests/Unit | PHPUnit\Framework\TestCase | しない | 計算やロジックだけのクラス(第1部のようなユニットテスト) |
tests/Feature | Tests\TestCase | する | DB・HTTPリクエスト・メールなど、Laravelの機能を使うテスト |
Tests\TestCase は、テストごとにLaravelアプリを起動してくれるクラスです。そのおかげでDBやルーティングが使えますが、その分だけ実行に時間がかかります。
第1部のCartは、そのまま Unit で動く
試しに、第3回で作った Cart クラスとそのテストを、Laravelプロジェクトに持ってきます。
Cart.phpをapp/Cart/Cart.phpに置く(名前空間はApp\Cartのまま)CartTest.phpをtests/Unit/CartTest.phpに置き、名前空間をTests\Unitに変える
Laravelの composer.json も App\ を app/ に対応させているので、Cart.php は1文字も変えずに動きます。テストも、名前空間の1行以外はそのままです。
テストスイート(テストのグループ)を指定して、Unitだけを実行してみます。
php artisan test --testsuite=Unit
PASS Tests\Unit\CartTest
✓ 空のカートの合計金額は0円
✓ 数量を指定すると単価×数量が合計金額になる
✓ 単価がマイナスなら例外を投げる
✓ 数量が0なら例外を投げる
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "0円は0円"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数が出ない"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数0.5は切り捨て"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数0.9も切り捨て"
Tests: 8 passed (11 assertions)
Duration: 0.08s
Laravelの中にあっても、フレームワークに依存しないクラスは、第1部と同じ普通のユニットテストで確かめられます。第4回で学んだ「外部に依存しない設計」は、フレームワークを使うときにもそのまま役立つのです。
php artisan testの表示では、第3回で付けた#[TestDox]の説明文ではなく、メソッド名とデータセット名が使われます。--testdoxオプションの表示とは別物です。
Unit のテストでは、DBは使えない
逆に、tests/Unit のテストでLaravelの機能を使おうとすると、エラーになります。試しに、Unitのテストの中でDBに注文を保存しようとした結果です。
FAIL Tests\Unit\FacadeTest
⨯ ユニットテストではデータベースは使えない 0.03s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Unit\FacadeTest > ユニットテストではデータベースは使… Error
Call to a member function connection() on null
PHPUnit\Framework\TestCase はLaravelアプリを起動しないため、DB接続が用意されていないのです。DBやHTTPを使うテストは、tests/Feature に書きましょう。
テストファイルは
php artisan make:test 〇〇Testで作れます。--unitを付けるとtests/Unitに、付けなければtests/Featureに作られます。
題材:注文API を作る
第6回ではリポジトリを手作りして注文を保存しました。今回は同じことを、LaravelのAPIとして作ります。
POST /api/orders:注文を登録する(メールアドレス・商品コード・数量を受け取る)GET /api/orders?email=...:メールアドレスで注文を絞り込んで返す
モデル・マイグレーション・Factoryを作る
-mf オプションを付けると、モデルと一緒に、マイグレーションとFactoryもまとめて作られます。
php artisan make:model Order -mf
INFO Model [app/Models/Order.php] created successfully.
INFO Factory [database/factories/OrderFactory.php] created successfully.
INFO Migration [database/migrations/2026_09_26_135627_create_orders_table.php] created successfully.
マイグレーションファイル名の日時の部分は、実行した日時によって変わります。
マイグレーションの up() に、第6回の schema.sql と同じ列を追加します。
public function up(): void
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->string('sku');
$table->unsignedInteger('quantity');
$table->timestamps();
});
}
モデル app/Models/Order.php では、一括で代入してよい列を #[Fillable] 属性で指定します。Laravel 13の新規プロジェクトでは、User モデルもこの書き方になっています。
<?php
namespace App\Models;
use Database\Factories\OrderFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['email', 'sku', 'quantity'])]
class Order extends Model
{
/** @use HasFactory<OrderFactory> */
use HasFactory;
}
以前のバージョンのLaravelでは、
protected $fillable = ['email', 'sku', 'quantity'];とプロパティで書いていました。こちらの書き方も引き続き使えます。
コントローラを作る
php artisan make:controller OrderController
app/Http/Controllers/OrderController.php を次のように書きます。入力のチェック(バリデーション)には、Laravelの validate() を使います。
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$validated = $request->validate([
'email' => ['required', 'email'],
]);
$orders = Order::where('email', $validated['email'])
->orderBy('id')
->get();
return response()->json($orders);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'email' => ['required', 'email'],
'sku' => ['required', 'string'],
'quantity' => ['required', 'integer', 'min:1'],
]);
$order = Order::create($validated);
return response()->json($order, 201);
}
}
store() では、数量に min:1 のルールを付けています。第3回で Cart に入れた「数量は1以上」のチェックと同じ仕様です。登録に成功したら、ステータスコード201(Created)で注文の内容を返します。
APIのルートを登録する
Laravel 13の新規プロジェクトには、API用のルートファイル routes/api.php がありません。自分で作成します。
<?php
use App\Http\Controllers\OrderController;
use Illuminate\Support\Facades\Route;
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
次に、bootstrap/app.php の withRouting() に api: の1行を追加して、このファイルを読み込ませます。
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
api: で登録したルートには、自動で /api が頭に付きます。登録できたか確認しましょう。
php artisan route:list --path=api
GET|HEAD api/orders ................................ OrderController@index
POST api/orders ................................ OrderController@store
Showing [2] routes
php artisan install:apiというコマンドでもroutes/api.phpを作れます。ただし、API認証用のパッケージ(Sanctum)も一緒にインストールされます。この回では認証を扱わないので、手で作る方法にしました。
HTTPテストで API を確かめる
テストファイルを作る
php artisan make:test OrderApiTest
--unit を付けていないので、tests/Feature/OrderApiTest.php に作られます。中身を次のように書き換えます。まずは注文登録の2つのテストです。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OrderApiTest extends TestCase
{
use RefreshDatabase;
public function test_注文を登録できる(): void
{
$response = $this->postJson('/api/orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 2,
]);
$response->assertCreated()
->assertJsonPath('email', 'taro@example.com')
->assertJsonPath('quantity', 2);
$this->assertDatabaseHas('orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 2,
]);
}
public function test_数量が0なら登録できない(): void
{
$response = $this->postJson('/api/orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 0,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['quantity']);
$this->assertDatabaseCount('orders', 0);
}
}
$this->postJson() は、APIにJSON形式でPOSTリクエストを送るメソッドです。実際にサーバーを立ち上げるのではなく、Laravelアプリの中でリクエストを処理するので、ブラウザもネットワークも使わずに速く実行できます。
返ってきたレスポンスには、Laravel独自のアサーションが使えます。よく使うものをまとめました。
| アサーション | 確かめること |
|---|---|
assertOk() | ステータスコードが200 |
assertCreated() | ステータスコードが201(登録成功) |
assertUnprocessable() | ステータスコードが422(入力エラー) |
assertJsonPath('キー', 値) | JSONの指定した位置の値が一致する(型も比較) |
assertJsonCount(件数) | JSONの配列の件数 |
assertJsonValidationErrors(['項目']) | 指定した項目に入力エラーがある |
$this->assertDatabaseHas('テーブル', [...]) | DBに、条件に合う行がある |
$this->assertDatabaseCount('テーブル', 件数) | DBのテーブルの行数 |
1つ目のテストでは、レスポンスの中身を確かめたうえで、assertDatabaseHas() でDBに本当に保存されたかも確かめています。第6回で「保存したものを取り出して確かめる」とした考え方と同じです。
2つ目のテストでは、エラーが返ることに加えて、assertDatabaseCount('orders', 0) で何も保存されていないことも確かめています。エラーを返しつつ、裏でデータが保存されてしまう、というバグを防ぐためです。
RefreshDatabase が、DBの準備と後片付けを引き受ける
テストクラスの先頭にある use RefreshDatabase; に注目してください。これがLaravel版の「DBの準備と後片付け」です。
試しに、この1行をコメントアウトして実行してみます。
FAIL Tests\Feature\OrderApiTest
⨯ 注文を登録できる 0.13s
⨯ 数量が0なら登録できない 0.01s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\OrderApiTest > 注文を登録できる
Expected response status code [201] but received 500.
Failed asserting that 500 is identical to 201.
The following exception occurred during the last request:
PDOException: SQLSTATE[HY000]: General error: 1 no such table: orders
「orders テーブルがない」というエラーです。テストではインメモリDBを使うので、誰かがテーブルを作らない限り、DBは空っぽなのです。
RefreshDatabase は、次の2つを自動でやってくれます。
- テストの実行前に、マイグレーションを実行してテーブルを作る
- 各テストをトランザクションの中で実行し、終わったらロールバックする
2つ目は、第6回で beginTransaction() と rollBack() を使って手作りした仕組みそのものです。インメモリDBでも、MySQLのような残るDBでも、テスト同士が干渉しません。
開発用のDB(
database/database.sqlite)にもordersテーブルを作るには、php artisan migrateを実行します。テストの方はRefreshDatabaseがテスト用DBにテーブルを作るので、このコマンドを実行していなくても動きます。
テストがバリデーション漏れを見つける
テストの効果を確かめるため、コントローラの数量のルールから min:1 を消してみます。
FAIL Tests\Feature\OrderApiTest
⨯ 数量が0なら登録できない 0.19s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\OrderApiTest > 数量が0なら登録できない
Expected response status code [422] but received 201.
Failed asserting that 201 is identical to 422.
at tests/Feature/OrderApiTest.php:40
36▕ 'sku' => 'APPLE-001',
37▕ 'quantity' => 0,
38▕ ]);
39▕
➜ 40▕ $response->assertUnprocessable()
41▕ ->assertJsonValidationErrors(['quantity']);
【スクショ:php artisan test で、失敗した行に矢印が付いて表示されている実行結果】
「422(入力エラー)のはずが、201(登録成功)が返ってきた」、つまり数量0の注文が登録されてしまったとわかります。php artisan test では、失敗したテストコードの該当行に ➜ の印が付いて表示されるので、どこで失敗したかが一目でわかります。
Factory でテストデータを用意する
絞り込みのテストには、DBにあらかじめ注文が入っている必要があります。そのためのテストデータを作るのがFactoryです。
Factory に「ありふれた値」を定義する
make:model のときに作られた database/factories/OrderFactory.php の definition() に、各項目のデフォルト値を書きます。
public function definition(): array
{
return [
'email' => fake()->safeEmail(),
'sku' => fake()->randomElement(['APPLE-001', 'ORANGE-001', 'GRAPE-001']),
'quantity' => fake()->numberBetween(1, 5),
];
}
fake() は、それらしいダミーデータを作ってくれる「Faker」というライブラリの機能です。safeEmail() なら、example.com などの実在しないドメインのメールアドレスを、ランダムに作ってくれます。
テストで使う
use App\Models\Order;
// ...
public function test_メールアドレスで注文を絞り込める(): void
{
Order::factory()->create(['email' => 'taro@example.com', 'sku' => 'APPLE-001']);
Order::factory()->create(['email' => 'hanako@example.com']);
Order::factory()->create(['email' => 'taro@example.com', 'sku' => 'GRAPE-001']);
$response = $this->getJson('/api/orders?email=taro@example.com');
$response->assertOk()
->assertJsonCount(2)
->assertJsonPath('0.sku', 'APPLE-001')
->assertJsonPath('1.sku', 'GRAPE-001');
}
Order::factory()->create([...]) は、definition() のデフォルト値をもとに注文を1件作り、DBに保存します。引数の配列で指定した項目だけが上書きされます。
第6回の saveOrder() ヘルパーと見比べてみてください。「関心のある項目だけを指定し、残りはデフォルト値に任せる」という考え方は、まったく同じです。Laravelでは、それがモデルごとに用意できる仕組みになっているわけです。
assertJsonPath('0.sku', ...)の0.skuは、「JSON配列の0番目の要素のsku」という意味です。ドット区切りで、入れ子になったJSONの奥まで指定できます。
同じデータを何件も作るなら、
Order::factory()->count(3)->create(['email' => 'taro@example.com'])のようにcount()で件数を指定できます。
PASS Tests\Feature\OrderApiTest
✓ 注文を登録できる 0.17s
✓ 数量が0なら登録できない 0.01s
✓ メールアドレスで注文を絞り込める 0.02s
第1部との対応表
この回で登場したLaravelの機能と、第1部で手作りした仕組みの対応をまとめます。
| やりたいこと | 第1部(素のPHP) | Laravel |
|---|---|---|
| テストではインメモリDBを使う | new PDO('sqlite::memory:')(第6回) | phpunit.xml の DB_CONNECTION と DB_DATABASE |
| テーブルを作る | schema.sql を exec()(第6回) | マイグレーション(RefreshDatabase が自動実行) |
| テスト後にデータを取り消す | beginTransaction() と rollBack()(第6回) | RefreshDatabase |
| テストデータを楽に作る | デフォルト引数つきの saveOrder()(第6回) | Factory |
| 保存されたかを確かめる | find() で取り出して比較(第6回) | assertDatabaseHas() |
| ロジックだけのテスト | PHPUnit\Framework\TestCase(第2〜4回) | tests/Unit(同じクラスを継承) |
Laravelの機能は便利ですが、中でやっていることは第1部と同じです。仕組みを知っていれば、テストがうまく動かないときにも「どこで何が起きているか」を見当づけられます。
今回の完成コード
今回作成・変更したファイルは次のとおりです。
shop/
├── app/
│ ├── Cart/
│ │ └── Cart.php ← 第3回からコピー
│ ├── Http/Controllers/
│ │ └── OrderController.php ← 追加
│ └── Models/
│ └── Order.php ← 追加
├── bootstrap/
│ └── app.php ← api: の1行を追加
├── database/
│ ├── factories/
│ │ └── OrderFactory.php ← 追加
│ └── migrations/
│ └── 2026_09_26_135627_create_orders_table.php ← 追加
├── routes/
│ └── api.php ← 追加
└── tests/
├── Feature/
│ └── OrderApiTest.php ← 追加
└── Unit/
└── CartTest.php ← 第3回からコピー
テストクラス以外は、本文に掲載したコードが完成形です。OrderApiTest.php は本文で分けて紹介したので、全体をまとめて載せておきます。
tests/Feature/OrderApiTest.php
<?php
namespace Tests\Feature;
use App\Models\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OrderApiTest extends TestCase
{
use RefreshDatabase;
public function test_注文を登録できる(): void
{
$response = $this->postJson('/api/orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 2,
]);
$response->assertCreated()
->assertJsonPath('email', 'taro@example.com')
->assertJsonPath('quantity', 2);
$this->assertDatabaseHas('orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 2,
]);
}
public function test_数量が0なら登録できない(): void
{
$response = $this->postJson('/api/orders', [
'email' => 'taro@example.com',
'sku' => 'APPLE-001',
'quantity' => 0,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['quantity']);
$this->assertDatabaseCount('orders', 0);
}
public function test_メールアドレスで注文を絞り込める(): void
{
Order::factory()->create(['email' => 'taro@example.com', 'sku' => 'APPLE-001']);
Order::factory()->create(['email' => 'hanako@example.com']);
Order::factory()->create(['email' => 'taro@example.com', 'sku' => 'GRAPE-001']);
$response = $this->getJson('/api/orders?email=taro@example.com');
$response->assertOk()
->assertJsonCount(2)
->assertJsonPath('0.sku', 'APPLE-001')
->assertJsonPath('1.sku', 'GRAPE-001');
}
}
サンプルのテストを削除したうえで、プロジェクト全体のテストを実行した結果です。
PASS Tests\Unit\CartTest
✓ 空のカートの合計金額は0円
✓ 数量を指定すると単価×数量が合計金額になる
✓ 単価がマイナスなら例外を投げる
✓ 数量が0なら例外を投げる
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "0円は0円"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数が出ない"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数0.5は切り捨て"
✓ 税込金額は10パーセントを加えて1円未満を切り捨てる with data set "端数0.9も切り捨て"
PASS Tests\Feature\OrderApiTest
✓ 注文を登録できる 0.17s
✓ 数量が0なら登録できない 0.01s
✓ メールアドレスで注文を絞り込める 0.02s
Tests: 11 passed (23 assertions)
Duration: 0.27s
【スクショ:php artisan test で Unit と Feature のテストがすべて PASS になった実行結果】
まとめ
- Laravelの
phpunit.xmlには、インメモリDBなどテスト用の設定が最初からそろっている tests/UnitはLaravelを起動しない普通のユニットテスト、tests/FeatureはLaravelを起動してDBやHTTPを使うテストpostJson()やgetJson()でAPIにリクエストを送り、assertCreated()やassertJsonPath()で検証するRefreshDatabaseが、テーブルの作成とテストごとのロールバックを引き受ける- Factoryで、関心のある項目だけを指定してテストデータを作れる
- どれも、第1部で手作りした仕組みと同じ考え方でできている
次回予告
第8回は「Laravelのフェイク機能とPest」です。第5回ではメール送信をモックで確かめましたが、Laravelには Mail::fake() や Queue::fake()、外部APIへの通信を差し替える Http::fake() といった、専用のフェイク機能が用意されています。後半では、テストをより短く書けるテストフレームワーク「Pest」を紹介し、同じテストをPHPUnit版とPest版で見比べます。
参考資料
- 第1回:なぜテストを書くのか
- 第2回:PHPUnitの環境構築と最初のテスト
- 第3回:読みやすいテストの書き方
- 第4回:テストしやすいコードの書き方
- 第5回:テストダブル
- 第6回:DBを使うテスト
- 第7回:Laravelのテスト基盤(今回)
- 第8回:Laravelのフェイク機能とPest
- 第9回:カバレッジとCI
- 第10回:TDDとレガシーコード (10/7 公開予定)
Analyzegear


