Laravel ゆるーくはじめるテストのチュートリアル
macOS14.5、Laravel11.9で検証。
前提としてフロントエンドはReact
、Inertia.js
とTypeScript
で開発。テストはDusk
を使ったブラウザテストとPHPUnit
のFeature
テストを行う。
プロジェクトセットアップ
プロジェクト名を指定してLaravelプロジェクトをローカルにダウンロード。ここではlarvel-testing-example
という名前にする。
$ curl -s "https://laravel.build/laravel-testing-example" | bash
プロジェクト起動
$ cd larvel-testing-example
$ ./vendor/bin/sail up
マイグレーションとシーディング
$ ./vendor/bin/sail artisan migrate --seed
フロントエンドはReact
とTypeScript
を使用する。認証パッケージのLaravel/Breeze
を使うとこれらのセットアップが容易なのでここではこれをインストールして開発サーバーを起動。
$ ./vendor/bin/sail composer require laravel/breeze --dev
$ ./vendor/bin/sail artisan breeze:install react --typescript
$ ./vendor/bin/sail npm run dev
http://localhost/login
にアクセスしてtest@example.com / password
でログイン後、dashboard画面に遷移できることを確認する
また、サンプルで用意されているUnit
テストとFeature
テストが成功することを確認しておく
$ ./vendor/bin/sail test
PASS Tests\Unit\ExampleTest
✓ that true is true 0.01s
・・・
Tests: 25 passed (61 assertions)
Duration: 1.46s
Duskセットアップ
続いて、ブラウザテストを行うためにdusk
をインストールする。
dusk
をインストール
$ ./vendor/bin/sail composer require laravel/dusk --dev
$ ./vendor/bin/sail artisan dusk:install
.env
から.env.dusk.local
を複製して以下を編集
APP_URL=http://laravel.test
DB_DATABASE=testing
MAIL_MAILER=log
テストを実行するとtests/Browser
以下にサンプルで用意されているテストが実行されるので成功することを確認しておく
$ ./vendor/bin/sail dusk
PASS Tests\Browser\ExampleTest
✓ basic example
・・・
Tests: 1 passed (1 assertions)
Duration: 2.10s
ブラウザテストを追加する
breeze
をインストールすると認証機能が使えるようになるので、ここではログインのテストを追加する。
$ ./vendor/bin/sail artisan dusk:make LoginTest
以下の例はname
属性がemail
とpaswword
なフォームにそれぞれ値をセットしてログインボタン押下でダッシュボード画面に遷移することを確認するテスト。
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
use DatabaseMigrations;
public function testログインできること(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->waitFor('button', 5)
->type('email', $user->email)
->type('password', 'password')
->click('button')
->pause(1000)
->assertPathIs('/dashboard');
});
}
}
ビルド後、テストすると追加したテストが実行されることが確認できる
$ ./vendor/bin/sail npm run build
$ ./vendor/bin/sail dusk
・・・
PASS Tests\Browser\LoginTest
✓ ログインできること
・・・
メール送信
breeze
で用意されているプロフィール画面にメール送信ボタンを追加して、ボタン押下でログイン中のユーザーにメールが送信されることをテストする。
※URLはhttp://localhost/profile
実装
$ ./vendor/bin/sail artisan make:mail UserNotify
また、Mailable
を使ってメール処理を実装するとテストを簡単に行うことができる。
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class UserNotify extends Mailable
{
use Queueable, SerializesModels;
public $user;
public function __construct($user)
{
$this->user = $user;
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'User Notify',
);
}
public function content(): Content
{
return new Content(
view: 'emails.user_notify',
);
}
public function attachments(): array
{
return [];
}
}
メールの内容を用意する
テストメール
また、ここでは動作確認のみ行うのでメールの出力先をログとする
MAIL_MAILER=log
画面とコントローラ部分は以下のとおり修正を加える。
フロントエンド
<div className="・・・">
<SendEmailForm className="・・・" />
</div>
import PrimaryButton from '@/Components/PrimaryButton';
import { useForm, usePage } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import { FormEventHandler } from 'react';
import { PageProps } from '@/types';
export default function SendEmailForm({ className = '' }: { className?: string }) {
const user = usePage<PageProps>().props.auth.user;
const { patch, processing, recentlySuccessful } = useForm();
const submit: FormEventHandler = (e) => {
e.preventDefault();
patch(route('profile.sendEmail'));
};
return (
<section className={className}>
<header>
<h2 className="・・・">Send Test Email</h2>
<p className="・・・">
ログイン中のユーザーにメールを送信します
</p>
</header>
<form onSubmit={submit} className="・・・">
<div className="・・・">
<PrimaryButton disabled={processing}>Send Email</PrimaryButton>
<Transition show={recentlySuccessful}>
<p className="・・・">Sended.</p>
</Transition>
</div>
</form>
</section>
);
}
ルーティングとコントローラー
Route::middleware('auth')->group(function () {
・・・
Route::patch('/profile/send-email', [ProfileController::class, 'sendEmail'])->name('profile.sendEmail');
});
class ProfileController extends Controller
{
・・・
public function sendEmail(Request $request): Response
{
$user = $request->user();
Mail::to($user->email)->send(new UserNotify($user));
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
}
ここまで実装したら、プロフィール画面に追加されたSend Email
ボタン押下で、ログにテストメール
が出力されることが確認できる
テスト
ブラウザテストを実装する。試したところ、メールの内容やメールが送信されことまでをdusk
でテストすることができないのでここでは画面の動きのみする。
$ ./vendor/bin/sail artisan dusk:make ProfileTest
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ProfileTest extends DuskTestCase
{
use DatabaseMigrations;
public function testテストメール送信ボタンが動作すること(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/profile')
->waitForText('Send Test Email')
->press('SEND EMAIL')
->waitForText('Sended.');
});
}
}
テストがパスすることを確認する
$ ./vendor/bin/sail dusk --filter=testテストメール送信ボタンが動作すること
PASS Tests\Browser\ProfileTest
✓ テストメール送信ボタンが動作すること 1.86s
・・・
続いて、メール送信部分のテストをtests/Feature/ProfileTest.php
に追加する。ここではログイン中のユーザーにメールが送信されることだけをテストするが、メールのタイトルや本文をテストすることも可能。
・・・
use App\Mail\UserNotify;
use Illuminate\Support\Facades\Mail;
class ProfileTest extends TestCase
{
・・・
public function testテストメール送信ボタンが動作すること()
{
// 実際にメール送信は行わない
Mail::fake();
$user = User::factory()->create();
$this->actingAs($user)
->patch('/profile/send-email')
->assertStatus(200);
Mail::assertSent(UserNotify::class, function ($mail) use ($user) {
// メールのタイトルと本文を取得できる
// $subject = $mail->subject;
// $body = $mail->render();
return $mail->hasTo($user->email);
});
}
}
テストがパスすることを確認する
$ ./vendor/bin/sail test --filter=testテストメール送信ボタンが動作すること
PASS Tests\Browser\ProfileTest
✓ テストメール送信ボタンが動作すること 1.86s
・・・
外部APIへのリクエスト
breeze
のプロフィール画面にRequest Api
ボタンを追加して、ボタン押下外部APIへリクエストを送信後、レスポンスが取得できることをテストする。
実装
フロントエンド
<div className="・・・">
<RequestExternalApiForm className="・・・" />
</div>
import PrimaryButton from '@/Components/PrimaryButton';
import { useForm, usePage } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import { FormEventHandler } from 'react';
import { PageProps } from '@/types';
export default function RequestExternalApiForm({ className = '' }: { className?: string }) {
const user = usePage<PageProps>().props.auth.user;
const { patch, processing, recentlySuccessful } = useForm();
const submit: FormEventHandler = (e) => {
e.preventDefault();
patch(route('profile.requestExternalApi'));
};
return (
<section className={className}>
<header>
<h2 className="・・・">Request External Api</h2>
<p className="・・・">
外部APIへリクエストします
</p>
</header>
<form onSubmit={submit} className="・・・">
<div className="・・・">
<PrimaryButton disabled={processing}>Request Api</PrimaryButton>
<Transition show={recentlySuccessful}>
<p className="・・・">Requested.</p>
</Transition>
</div>
</form>
</section>
);
}
ルーティングとコントローラー
Route::middleware('auth')->group(function () {
・・・
Route::patch('/profile/request-external-api', [ProfileController::class, 'requestExternalApi'])->name('profile.requestExternalApi');
});
ここでは外部APIとしてjsonplaceholder
を利用する。レスポンスは確認のためログに出力する。また、Inertia
のレスポンスに外部APIから取得したtitle
フィールドを返すこととする。
class ProfileController extends Controller
{
・・・
public function requestExternalApi(Request $request): Response
{
$response = Http::get('https://jsonplaceholder.typicode.com/todos/1');
$body = json_decode($response->getBody(), true);
Log::info($body);
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
'title' => $body['title'], // ★レスポンスにAPIから取得したtitleを返すようにする
]);
}
}
ここまで実装したら、プロフィール画面に追加されたRequest Api
ボタン押下で、ログに外部APIのレスポンスが出力されることが確認できる
[2024-07-29 08:23:59] local.INFO: array (
'userId' => 1,
'id' => 1,
'title' => 'delectus aut autem',
'completed' => false,
)
テスト
ブラウザテストは前述のメール送信のテストとほぼ同じ内容のため、ここでは割愛する。試したところ、API
をモック化する部分をdusk
でテストすることができないのでFeature
テストのみを実装する
<?php
・・・
use Illuminate\Support\Facades\Http;
class ProfileTest extends DuskTestCase
{
・・・
public function test外部APIからデータを取得すること()
{
Http::fake([
'https://jsonplaceholder.typicode.com/todos/1' => Http::response([
'title' => 'Test Title',
], 200)
]);
$user = User::factory()->create();
$response = $this->actingAs($user)
->patch('/profile/request-external-api')
->assertStatus(200);
$response->assertInertia(function($page) {
$page->component('Profile/Edit')
->where('title', 'Test Title'); // ★モックでセットした値がレスポンスとして返ってくることをテストする
});
}
}
テストがパスすることを確認する
$ ./vendor/bin/sail test --filter=test外部APIからデータを取得すること
PASS Tests\Browser\ProfileTest
✓ テストメール送信ボタンが動作すること 1.86s
・・・
バッチ処理
実装
ここでは、登録中のユーザーにメールを送信するコマンドを実装する
$ ./vendor/bin/sail artisan make:command SendEmails
<?php
namespace App\Console\Commands;
use App\Mail\UserNotify;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendEmails extends Command
{
protected $signature = 'app:send-emails';
protected $description = 'Command description';
public function handle()
{
User::all()->each(function($user) {
Mail::to($user->email)->send(new UserNotify($user));
});
}
}
コマンドを実行してメール送信に成功するとログに出力される
$ ./vendor/bin/sail artisan app:send-emails
テスト
$ ./vendor/bin/sail artisan make:test SendEmailsTest
ここではユーザーを10人登録して、全員にメールが送信されることをテストする
<?php
namespace Tests\Feature;
use App\Console\Commands\SendEmails;
use App\Mail\UserNotify;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class SendEmailsTest extends TestCase
{
use RefreshDatabase;
public function testユーザーにメール送信されること(): void
{
Mail::fake();
$user = User::factory(10)->create();
Artisan::call(SendEmails::class);
Mail::assertSent(UserNotify::class, 10);
}
テストがパスすることを確認する
$ ./vendor/bin/sail test --filter=testユーザーにメール送信されること
PASS Tests\Feature\SendEmailsTest
✓ ユーザーにメール送信されること 0.38s
・・・
GitHub Actionsでテスト
公式ドキュメントにGitHub Actions
でテストするためのサンプルがあるのでこれを参考にする。
動いたサンプル
name: CI
on: [push]
jobs:
dusk-php:
runs-on: ubuntu-latest
steps:
- name: Create Database
run: |
sudo systemctl start mysql
mysql -uroot -proot -e "CREATE DATABASE testing"
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Composer Install
run: |
sed -i \
-e 's|APP_URL=http://laravel.test|APP_URL=http://localhost|g' \
-e 's|DB_HOST=mysql|DB_HOST=localhost|g' \
-e 's|DB_USERNAME=sail|DB_USERNAME=root|g' \
-e 's|DB_PASSWORD=password|DB_PASSWORD=root|g' \
.env.dusk.local
cp .env.dusk.local .env
composer install --no-progress --prefer-dist --optimize-autoloader
- name: Npm Build
run: |
npm install
npm run build
- name: Upgrade and Start Chrome Driver
run: |
php artisan dusk:chrome-driver --detect
./vendor/laravel/dusk/bin/chromedriver-linux &
- name: Run Laravel Server
run: sudo php artisan serve --port=80 --no-reload &
- name: Run Dusk Tests
run: php artisan dusk
GitLab Runnerでテスト
GitLab Runner
でdusk
を使ったテストをするためのDockerイメージとサンプルを作ってくれている人がいるのでこれを利用する。
これを使ったサンプル
.gitlab-ci.yml
stages:
- build
- test
variables:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: sail
MYSQL_PASSWORD: password
MYSQL_DATABASE: testing
DB_HOST: mysql
DB_CONNECTION: mysql
build:
stage: build
image: chilio/laravel-dusk-ci:stable
script:
- composer install --no-progress --prefer-dist --optimize-autoloader
- npm install
- npm run build
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- vendor
- node_modules
artifacts:
paths:
- public/build/*
test:
stage: test
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- vendor
- node_modules
policy: pull
services:
- name: mysql:8.4
alias: mysql-test
image: chilio/laravel-dusk-ci:stable
script:
- sed -i 's|APP_URL=http://laravel.test|APP_URL=http://localhost|g' .env.dusk.local
- cp .env.dusk.local .env
- configure-laravel
- start-nginx-ci-project
- php artisan dusk:install
- php artisan dusk --colors --debug
artifacts:
paths:
- ./storage/logs
- ./tests/Browser/screenshots
- ./tests/Browser/console
- ./public/build
when: always
その他
できるところはブラウザテストで、それが難しい場合はFeature
テストを書くというゆるーい方針で書きました。
今回使用したリポジトリ
Discussion