0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel Inertia + Vue】ほなフロントもバックもテスト書きましょか(Pest, Vitest)

Last updated at Posted at 2025-12-23

大事なところ。でもそんなにお話することもない。
テスト技法の解説ではなく、テスト環境を用意する解説です。

あ、E2Eテストは解説しません!

バックエンド

Laravel は最近 PHPUnit から Pest に変わりました。
名前のとおり JavaScript の有名なやつ JestPHP 版ですね。

とはいっても、今までの 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 系には使えないです。
もし使いたい場合はモックしましょう

例えばこんな感じの、時間の範囲を測定するへルパーを作成します。

app/Libs/DateTimeRange.php
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 に正しい値を入れます。

tests/Unit/DateTimeRangeTest.php
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 に何やらできているので、ブラウザで見てみましょう。

image.png

ちゃんとテストが動いているところは緑、一度も実行されていないところは赤で表示されます。
これを見ながら、すべての処理が緑になるようにテストを作成していきましょう~。
色塗りの要領です!!

とはいえ、緑にするテストを書くことは何の意味もないどころか、バグを隠匿しかねません。エッジケースなどを考慮しながら、実際の利用環境を想定してテスト用の値を作成しましょう。

フューチャーテスト

Laravel にはもう一つフューチャーテストがあります。
こっちはモデルからコントローラーまで、どんなテストも記載することが可能です。

コントローラーのアクションごとにテストを書いたり、実環境を想定して API の動作テストを書いてもよいです。

機能を修正してもテストがそのまま生かせるようなコードが書けていれば成功です!
つまり、細かい挙動ではなく、おおまかに入ってくる値と出ていく値が検証できれば良いですね。

▼ データベースを利用する

フィーチャーテストでは、データベースを利用することも多いです。
利用されるデータべースなどの設定は phpunit.xml に記載されています。
デフォルトではインメモリの sqlite が使われます。

もし MySQLPostgreSQL 専用の機能を使ったシステムであれば、切り替えてください。

phpunit.xml
    <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 配列を返却するルートを考えます。

routes/web.php
use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts', function () {
    $posts = Post::all();

    return response()->json([
        'posts' => $posts->toArray(),
    ]);
});

これを検証するには、いくつかのデータを作ったうえで、そのデータが取得できるかを見れば良いわけです。

tests/Feature/PostTest.php
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 仕様にします。

routes/web.php
use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts', function () {
    return Inertia::render('Posts/Index', [
        'posts' => fn () => Post::all(),
    ]);
});

ポイントは assertInertia() です。これを使うと Inertiaprops のデータを抜き出すことができます。JSON の検証方法は API の時と同じですね。

tests/Feature/PostTest.php
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 ファイルが無いです。

▼ データセットでの検証

データのバリデーションやステータスを使った処理を書きたいときに、同じテストを複数の条件で実行したい場合があります。
そんな機能もちゃんと備わっています。

tests/Unit/CalculateTest.php
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 に登録します。

tsconfig.json
  "compilerOptions": {
        "types": [
            "vite/client",
            "./resources/js/types",
+           "vitest/globals"
        ]
    }

そして Vite に登録します。

vite.config.ts
-   import { defineConfig } from 'vite'
+   import { defineConfig } from 'vitest/config'
    
    export default defineConfig({
+     test: {
+       globals: true,
+       environment: 'happy-dom',
+       coverage: {
+         provider: 'v8',
+         reporter: ['html'],
+       },
+     },
      // ...

最後に Vitestpackage.json に登録します。

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 などが検証の対象となります。

試しに、ただのカウンターを検証してみましょう。

resources/js/useCounter.ts
import { ref } from 'vue'

export const useCounter = () => {
  const number = ref<number>(0)

  const count = () => {
    number.value++
  }

  return {
    number,
    count,
  }
}

ファイルの末尾に .test.ts を含めることでテストファイルとなり、自動的に実行されます。対象ファイルと同じ名前にすることが多いですね。

Laravel 環境では resources/js/** 以下が探索範囲のため、こちらに配置しましょう。

resources/js/useCounter.test.ts
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 に何やらできています。
ブラウザで見てみると、バックエンドと同じような画面が閲覧できるかと思います。

image.png

フィーチャーテスト

Vue のフィーチャーテストは Component のテストが該当しますかね。

Laravel の場合は、Page 要素も Component として検証することができます。
こんな感じのただ表示する Page を作成しました。

resources/js/pages/Posts/Index.vue
<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> を検証してみます。

resources/js/pages/Posts/posts-index.test.ts
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)

おわりに

現代のシステムは複雑怪奇になっていて、とても一個人が紐解けるみのではありません...。
有名なライブラリは、そのライブラリの中でテストを作ってます。

なので自分で作ったとこをテストするように書きましょう。
テストの本質は別の記事や本を参照してくださいね!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?