大事なところ。でもそんなにお話することもない。
テスト技法の解説ではなく、テスト環境を用意する解説です。
あ、E2Eテストは解説しません!
バックエンド
Laravel は最近 PHPUnit から Pest に変わりました。
名前のとおり JavaScript の有名なやつ Jest の PHP 版ですね。
とはいっても、今までの assert の書き方も書けますし、Jest の expect って書き方も書けます。いいとこどりですね。
何より class に縛られなくてよくなったのが神です。
主要なコマンド
主要なコマンドを以下に乗せておきます。
# Feature Test の作成
$ php artisan make:test XxxTest
# Unit Test の作成
$ php artisan make:test XxxTest --unit
# すべてのテストを実行
$ php artisan test
# 特定の文字が含まれたファイルパスに限って、テストを実行
$ php artisan test --filter="PostTest"
# カバレッジの測定
$ php artisan test --coverage
# カバレッジの測定(HTML出力)
$ php artisan test --coverage-html ./coverage
ユニットテスト
これは単体テストってやつで、一つのクラスやファイルなどを確実に動作することを保証するために利用します。
単体テストなので、Laravel の機能は一切利用できないです。
なので、ライブラリに近い処理や、ヘルパークラスのみの利用に限られます。
Route、Controller、Model や DI 系には使えないです。
もし使いたい場合はモックしましょう
例えばこんな感じの、時間の範囲を測定するへルパーを作成します。
namespace App\Libs;
use Illuminate\Support\Carbon;
class DateTimeRange
{
public function __construct(
public Carbon $start,
public Carbon $end,
) {}
public function hours(): float
{
return $this->start->diffInHours($this->end);
}
public function minutes(): float
{
return $this->start->diffInMinutes($this->end);
}
}
これを検証するテストを作成します。
テストは expect に確認したい要素を入れて、 toBe に正しい値を入れます。
use App\Libs\DateTimeRange;
use Illuminate\Support\Carbon;
test('経過分の取得', function () {
$t1 = Carbon::make('2025-12-01 10:00');
$t2 = Carbon::make('2025-12-01 13:30');
$range = new DateTimeRange($t1, $t2);
expect($range->hours())->toBe(3.5);
});
これを実行することで、正しい動作が行われるか検証することができます。
$ php artisan test --filter="DateTimeRange"
PASS Tests\Unit\DateTimeRangeTest
✓ 経過分の取得 0.01s
Tests: 1 passed (1 assertions)
Duration: 0.04s
▼ カバレッジの確認の仕方
テストと切っても切り離せないカバレッジ。
これは、すべての処理がテストでカバーされているかを分かりやすく数値にしたものです。
coverage の実行には xDebug が必須なので、環境に合わせた手法で導入してください。
xdebug.mode=coverageを忘れずに...(一敗)
$ php artisan test --coverage-html
# これでもよい
XDEBUG_MODE=coverage php artisan test --coverage-html ./coverage
すると ./coverage/index.html に何やらできているので、ブラウザで見てみましょう。
ちゃんとテストが動いているところは緑、一度も実行されていないところは赤で表示されます。
これを見ながら、すべての処理が緑になるようにテストを作成していきましょう~。
色塗りの要領です!!
とはいえ、緑にするテストを書くことは何の意味もないどころか、バグを隠匿しかねません。エッジケースなどを考慮しながら、実際の利用環境を想定してテスト用の値を作成しましょう。
フューチャーテスト
Laravel にはもう一つフューチャーテストがあります。
こっちはモデルからコントローラーまで、どんなテストも記載することが可能です。
コントローラーのアクションごとにテストを書いたり、実環境を想定して API の動作テストを書いてもよいです。
機能を修正してもテストがそのまま生かせるようなコードが書けていれば成功です!
つまり、細かい挙動ではなく、おおまかに入ってくる値と出ていく値が検証できれば良いですね。
▼ データベースを利用する
フィーチャーテストでは、データベースを利用することも多いです。
利用されるデータべースなどの設定は phpunit.xml に記載されています。
デフォルトではインメモリの sqlite が使われます。
もし MySQL や PostgreSQL 専用の機能を使ったシステムであれば、切り替えてください。
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<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"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
そして、ファイルの先頭に RefreshDatabase を記載してみてください。
test() のくくりごとに、自動的にデータベースをリセットしてくれます。
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->use(RefreshDatabase::class);
また Seeder を利用したい場合は、テストの中で $this->seed() を実行してください。
テスト用のデータセットや、定数テーブルがある場合に利用すると良いです。
// DatabaseSeederを実行
$this->seed();
// 指定のシーダを実行
$this->seed(OrderStatusSeeder::class);
// 各テストで常に実行
beforeEach(function () {
$this->seed();
});
▼ APIを検証する
では実際に簡単なテストを書いてみましょう。
Post 配列を返却するルートを考えます。
use App\Models\Post;
use Illuminate\Support\Facades\Route;
Route::get('/posts', function () {
$posts = Post::all();
return response()->json([
'posts' => $posts->toArray(),
]);
});
これを検証するには、いくつかのデータを作ったうえで、そのデータが取得できるかを見れば良いわけです。
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\Fluent\AssertableJson;
pest()->use(RefreshDatabase::class);
test('Postの取得', function () {
$user1 = User::factory()->create(['name' => 'Aさん']);
$user2 = User::factory()->create(['name' => 'Bさん']);
$post1 = Post::create(['user_id' => $user1->id, 'title' => 'AAAAA']);
$post2 = Post::create(['user_id' => $user2->id, 'title' => 'BBBBB']);
$this->getJson('/posts')
->assertStatus(200) // success
->assertJson(fn (AssertableJson $json) =>
// posts の長さが 2 で、先頭の要素を検証する
$json->has('posts', 2, fn (AssertableJson $post) =>
$post->has('id') // ID を持つ
->where('user_id', $post1->id) // user_id の検証
->where('title', 'AAAAA') // title の検証
->etc() // ほかの要素も持つ(無いとエラー)
)
);
});
これを実行してみると、正しい値が返ってきていることが分かります。
$ php artisan test --filter="PostTest"
PASS Tests\Feature\PostTest
✓ Postの取得
Tests: 1 passed (17 assertions)
Duration: 0.15s
▼ Inertia の検証
Inertia を利用していても、通信結果の検証が可能です。
ただ、ロジックの検証はできないため、正しいレスポンスが作成できているか、という視点のテストとなります。
先ほどのルートを改造して、Inertia 仕様にします。
use App\Models\Post;
use Illuminate\Support\Facades\Route;
Route::get('/posts', function () {
return Inertia::render('Posts/Index', [
'posts' => fn () => Post::all(),
]);
});
ポイントは assertInertia() です。これを使うと Inertia の props のデータを抜き出すことができます。JSON の検証方法は API の時と同じですね。
use Inertia\Testing\AssertableInertia;
test('Postの取得(Inertia)', function () {
$user1 = User::factory()->create(['name' => 'Aさん']);
$user2 = User::factory()->create(['name' => 'Bさん']);
$post1 = Post::create(['user_id' => $user1->id, 'title' => 'AAAAA']);
$post2 = Post::create(['user_id' => $user2->id, 'title' => 'BBBBB']);
$this->get('/posts')
->assertStatus(200) // success
->assertInertia(fn (AssertableInertia $page) => $page
->component('Posts/Index')
->has('posts', 2, fn (AssertableJson $post) =>
$post->has('id')
->where('user_id', $post1->id)
->where('title', 'AAAAA')
->where('user_id', 1)
->etc()
)
);
});
Unable to locate file in Vite manifest: resources/js/pages/Posts/Index.vue. (View: .../resources/views/app.blade.php)
もーしこのエラーが表示されたら、フロントを build するか、dev サーバーを起動しておきましょう。
生成済みの JS ファイルが無いです。
▼ データセットでの検証
データのバリデーションやステータスを使った処理を書きたいときに、同じテストを複数の条件で実行したい場合があります。
そんな機能もちゃんと備わっています。
function add(int $a, int $b)
{
return $a + $b;
}
test('足し算のテスト', function (int $a, int $b, int $sum) {
expect(add($a, $b))->toBe($sum);
})->with([
'正の数' => [1, 2, 3],
'負の数' => [-1, 4, 3],
'0含有正の数' => [0, 5, 5],
'0含負正の数' => [-3, 0, -3],
]);
with() には配列を指定することができるのですが、ここで key => value という形にしておくと、コンソールにそれぞれのテストの詳細が表示されます。
見ていて楽しいのでおすすめです。
$ php artisan test --filter="CalculateTest"
PASS Tests\Unit\CalculateTest
✓ 足し算のテスト with dataset "正の数"
✓ 足し算のテスト with dataset "負の数"
✓ 足し算のテスト with dataset "0含有正の数"
✓ 足し算のテスト with dataset "0含負正の数"
Tests: 4 passed (4 assertions)
Duration: 0.03s
フロントエンド
フロントエンドの検証には色々なフレームワークがありますが、現代であれば Vitest を使っておけばいいと思います。
これも Jest の派生ライブラリのため、Pest とも書き方が似ており、親和性が高いです。
インストール
Vitest のほかに Vue Test Utils というライブラリも入れておきます。
これは Vue のコンポーネントの検証の補助をしてくれるものです。
そしてカバレッジ測定用に v8 エンジンも入れておきます。
$ npm install -D vitest happy-dom @testing-library/vue
# カバレッジ用
$ npm i -D @vitest/coverage-v8
テストの型を TypeScript に登録します。
"compilerOptions": {
"types": [
"vite/client",
"./resources/js/types",
+ "vitest/globals"
]
}
そして Vite に登録します。
- import { defineConfig } from 'vite'
+ import { defineConfig } from 'vitest/config'
export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'happy-dom',
+ coverage: {
+ provider: 'v8',
+ reporter: ['html'],
+ },
+ },
// ...
最後に Vitest を package.json に登録します。
"scripts": {
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
"dev": "vite",
"lint": "eslint . --fix",
+ "test": "vitest run"
},
run を付与しないと、自動的に watch モードになります。
主要なコマンド
主要なコマンドを以下に乗せておきます。
# すべてのテストを実行(xxx.test.ts)
$ npm run test
# 特定の文字が含まれたファイルパスに限って、テストを実行
$ npm run test -t="sample"
# カバレッジの測定(HTMLも同時出力)
$ npm run test -- --coverage
ユニットテスト
Vitest のユニットテストは、Pest と全く同じなので細かい説明は省略します。
Composable などが検証の対象となります。
試しに、ただのカウンターを検証してみましょう。
import { ref } from 'vue'
export const useCounter = () => {
const number = ref<number>(0)
const count = () => {
number.value++
}
return {
number,
count,
}
}
ファイルの末尾に .test.ts を含めることでテストファイルとなり、自動的に実行されます。対象ファイルと同じ名前にすることが多いですね。
Laravel 環境では resources/js/** 以下が探索範囲のため、こちらに配置しましょう。
import { useCounter } from '@/useCounter'
test('test', () => {
const counter = useCounter()
expect(counter.number.value).toBe(0)
counter.count()
expect(counter.number.value).toBe(1)
})
これを実行すると、同様にテスト結果が表示されます。
$ npm run test
RUN v4.0.16 /home/wsl/code/test-inertia
✓ resources/js/useCounter.test.ts (1 test) 1ms
✓ test 1ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 23:06:59
Duration 362ms (transform 20ms, setup 0ms, import 66ms, tests 1ms, environment 171ms)
▼ カバレッジの確認の仕方
Vitest もカバレッジの出力ができます。
設定方法はインストールの章で終わっていますね。
$ npm run test -- --coverage
このコマンドを実行すると、./coverage/index.html に何やらできています。
ブラウザで見てみると、バックエンドと同じような画面が閲覧できるかと思います。
フィーチャーテスト
Vue のフィーチャーテストは Component のテストが該当しますかね。
Laravel の場合は、Page 要素も Component として検証することができます。
こんな感じのただ表示する Page を作成しました。
<template>
<div class="m-4">
<pre>{{ posts }}</pre>
</div>
</template>
<script setup lang="ts">
defineProps<{
posts: {
id: number
user_id: number
title: string
}[]
}>()
</script>
defineProps と HTML 上の <pre> を検証してみます。
import { mount } from '@vue/test-utils'
import { test } from 'vitest'
import PostsIndex from '@/pages/Posts/Index.vue'
test('posts の値チェック', () => {
const posts: {
id: number
user_id: number
title: string
}[] = [
{ id: 1, user_id: 1, title: 'AAAAA' },
{ id: 2, user_id: 2, title: 'BBBBB' },
{ id: 3, user_id: 3, title: 'CCCCC' },
]
const wrapper = mount(PostsIndex, {
props: {
posts,
},
})
// 値が props で渡せているか
expect(wrapper.props('posts')).toEqual(posts)
// <pre> に値が表示されているか
const pre = wrapper.find('pre')
expect(pre.text()).toContain('AAAAA')
expect(pre.text()).toContain('BBBBB')
})
これを実行してみると Component の検証ができていることが分かります。
$ npm run test -t "posts-index"
RUN v4.0.16 /home/wsl/code/test-inertia
✓ resources/js/pages/Posts/posts-index.test.ts (1 test) 9ms
✓ posts の値チェック 9ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 23:34:26
Duration 364ms (transform 40ms, setup 0ms, import 91ms, tests 9ms, environment 177ms)
おわりに
現代のシステムは複雑怪奇になっていて、とても一個人が紐解けるみのではありません...。
有名なライブラリは、そのライブラリの中でテストを作ってます。
なので自分で作ったとこをテストするように書きましょう。
テストの本質は別の記事や本を参照してくださいね!

