はじめに
「テストってどうやるの?」という疑問からテストケースを作成し、実際に手動テストを実施してみました。すると、想像以上にたくさんの不具合が見つかりました。
この記事では、その経験を通じて学んだ「手動テストの重要性」についてまとめます。
テスト対象アプリ
子ども予防接種スケジューラーアプリ(Laravel + Docker + MySQL)
主な機能:
- 認証機能(登録・ログイン・パスワードリセット)
- 子ども管理(登録・編集・削除・ゴミ箱)
- 接種スケジュール自動生成
- 副反応記録
- 医療機関管理
- 予約管理
- 家族共有(招待コードでグループ参加)
- 母子手帳写真アップロード
- プッシュ通知・リマインダー送信
テストの進め方
Excelでテストケースを作成し、1項目ずつ手動で実施しました。各テスト項目には「No・機能・テスト項目・操作手順・期待結果・結果・備考」を記入し、結果は ○(正常)/ ×(不具合)で記録しました。
テスト結果と発見した不具合
1. 認証機能のテスト(8項目)
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 新規登録 | 正常登録 | ○ |
| 2 | 新規登録 | メール重複エラー | ○ |
| 3 | 新規登録 | 必須項目未入力 | ○ |
| 4 | ログイン | 正常ログイン | ○ |
| 5 | ログイン | パスワード誤り | ○ |
| 6 | ログアウト | 正常ログアウト | ○ |
| 7 | パスワードリセット | リセットメール送信 | × |
| 8 | パスワード変更 | 正常変更 | ○ |
❌ No.7 パスワードリセット:Mailpitの設定がなかった
開発用のメール受信環境(Mailpit)がそもそも設定されていなかったため、リセットメールが送信できませんでした。
Mailpitとは?
本物のメールを送らずに、ブラウザ上でメールの受信内容を確認できる開発用のダミーメールサーバーです。
解決方法: docker-compose.yml に Mailpit サービスを追加し、.env のメール設定を修正。
mailpit:
image: axllent/mailpit
ports:
- "8025:8025"
- "1025:1025"
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
→ http://localhost:8025 でリセットメールの受信を確認できるようになりました。
2. 子ども管理のテスト(7項目)
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 子ども登録 | 正常登録 | △ |
| 2 | 子ども登録 | 必須項目未入力 | ○ |
| 3 | 子ども登録 | ニックネーム50文字超 | ○ |
| 4 | 子ども編集 | 正常編集 | ○ |
| 5 | 子ども削除 | ソフトデリート | ○ |
| 6 | ゴミ箱 | 復元 | ○ |
| 7 | ゴミ箱 | 完全削除 | ○ |
⚠️ No.1 正常登録:シーダーの未実行でワクチンデータが空
docker compose down でコンテナを停止するとデータベースがリセットされ、ワクチンデータが0件になっていました。初回は必ずシーダーを実行する必要があります。
php artisan db:seed
→ 11種類のワクチンデータが登録され、接種スケジュールが自動生成されました。
3. 子ども編集:バグと仕様の違いを学んだ
子どもの生年月日を編集しても、接種スケジュールが再生成されないことがわかりました。最初はバグかと思いましたが、これは仕様でした。
| 分類 | 内容 |
|---|---|
| バグ | 意図していない動作・エラー |
| 仕様 | 最初からそういう設計になっている |
今回は「生年月日を編集してもスケジュールは変わらない」という設計になっていました。ただし、利便性のために ChildController.php を修正して生年月日編集時にスケジュールを自動再生成するよう改善しました。
4. 接種スケジュールのテスト(5項目)
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | スケジュール自動生成 | 子ども登録時に生成 | ○ |
| 2 | 接種済みにする | 正常登録 | ○ |
| 3 | 接種済みにする | 日付未入力 | × |
| 4 | ダッシュボード | 期限切れアラート表示 | ○ |
| 5 | ダッシュボード | もうすぐ表示 | ○ |
No.1では定期接種9種類・任意接種2種類、合計11種類のスケジュールが自動生成されることを確認しました。
❌ No.3 接種済みにする:日付未入力で全行にエラーが表示される
日付を入力せずに「接種済みにする」ボタンを押すと、押したワクチンの行だけでなく全行にバリデーションエラーが表示されるバグがありました。
原因: フォームのIDが全て同じになっていたため、どの行のエラーか区別できていなかった。
解決方法:
-
VaccinationScheduleController.phpでエラー発生時にスケジュールIDをセッションに保存 -
index.blade.phpで該当行のIDと一致する場合のみエラーを表示するよう修正
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->with('error_schedule_id', $schedule->id)
->withInput();
}
→ 「接種日は必須です。」が押したワクチンの行だけに表示されるようになりました。✅
5. 副反応記録のテスト(4項目)→ 全て○
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 副反応登録 | 正常登録 | ○ |
| 2 | 副反応登録 | 必須項目未入力 | ○ |
| 3 | 副反応登録 | 終了日が開始日より前 | ○ |
| 4 | 副反応削除 | 正常削除 | ○ |
6. 医療機関管理のテスト(5項目)→ 全て○
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 医療機関登録 | 正常登録 | ○ |
| 2 | 医療機関登録 | 必須項目未入力 | ○ |
| 3 | 医療機関登録 | 名前100文字超 | ○ |
| 4 | 医療機関編集 | 正常編集 | ○ |
| 5 | 医療機関削除 | 正常削除 | ○ |
7. 予約管理のテスト(4項目)→ 全て○
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 予約登録 | 正常登録 | ○ |
| 2 | 予約登録 | 必須項目未入力 | ○ |
| 3 | 予約登録 | 時間フォーマット誤り | ○ |
| 4 | 予約削除 | 正常削除 | ○ |
No.3はタイムピッカー(時刻選択UI)を使用しているため、そもそも不正な形式の入力自体ができない設計になっていました。バリデーションエラーを出す前に、UIレベルで防御していたケースです。
8. 家族共有のテスト(6項目)
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | グループ作成 | 正常作成 | ○ |
| 2 | グループ参加 | 正しい招待コードで参加 | × |
| 3 | グループ参加 | 誤った招待コード | ○ |
| 4 | グループ参加 | 招待コード未入力 | ○ |
| 5 | メンバー削除 | 正常削除 | × |
| 6 | グループ削除 | 正常削除 | ○ |
❌ バグ①:同一ユーザーが同じグループに重複参加できる(No.2)
招待コードでの参加ボタンを2回押すと、同じユーザーが同じグループに重複登録されてしまいました。
原因1:コントローラーのキャッシュ問題
コード上では重複チェックの処理が書かれていましたが、$group->members はキャッシュされたデータを参照するため、最新の状態が反映されていませんでした。
// 修正前(キャッシュされたデータで確認)
if ($group->members->contains(Auth::id())) { ... }
// 修正後(データベースに直接確認)
if ($group->members()->where('user_id', Auth::id())->exists()) { ... }
キャッシュとは?
一度取得したデータを一時的に保存しておく仕組みです。便利な反面、データが変わっても古い情報を使い続けてしまうことがあります。コンビニの在庫確認で例えると、「さっき5個あったから今も5個」と思い込んでしまうような状態です。
原因2:データベースにユニーク制約がなかった
コントローラーを修正しても重複参加が止まらなかったため、データベースを直接確認しました。
family_group_id: 3, user_id: 22 → 2件登録されていた!
family_group_members テーブルにユニーク制約がなかったため、同じ組み合わせが重複して登録できてしまっていました。
ユニーク制約とは?
同じデータが重複して登録されないようにする制限のことです。メールアドレスで例えると「同じアドレスを2人が使えないようにする仕組み」と同じです。
新しいマイグレーションファイルを作成してユニーク制約を追加しましたが、すでにデータベースに重複データが存在していたためマイグレーションが失敗。tinkerで重複レコードを手動削除してから再実行しました。
php artisan tinker
DB::table('family_group_members')->where('id', 7)->delete();
exit
php artisan migrate
$table->unique(['family_group_id', 'user_id']); // ← 追加
→「すでに参加しています。」と表示され、1件しか登録されなくなりました。✅
❌ バグ②:メンバー削除時に同一ユーザーの全レコードが削除される(No.5)
片方の削除ボタンを押したところ、2件とも削除されてメンバーが0人になってしまいました。
原因: バグ①による重複データが存在していたため、detach($userId) が同じユーザーIDの全レコードを削除してしまっていた。
解決: バグ①の修正(重複参加防止)により根本的に解決しました。✅
9. 母子手帳写真アップロードのテスト(5項目)→ 全て○
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 写真アップロード | 正常アップロード | ○ |
| 2 | 写真アップロード | ファイル未選択 | ○ |
| 3 | 写真アップロード | 5MB超のファイル | ○ |
| 4 | 写真アップロード | 非対応形式(gif) | ○ |
| 5 | 写真削除 | 正常削除 | ○ |
テスト用のダミーファイルをWindowsのコマンドプロンプトで作成しました。
# 5MB超のダミーファイルを作成
fsutil file createnew test.jpg 5242881
# gif形式のダミーファイルを作成
fsutil file createnew test.gif 1000
このコマンドの存在も、テストを通じて初めて知りました。また、gif形式(アニメーション画像によく使われるファイル形式)も、このテストで初めて意識しました。
10. プッシュ通知のテスト(4項目)
| No | 機能 | テスト項目 | 結果 |
|---|---|---|---|
| 1 | 通知有効化 | 正常有効化 | ○ |
| 2 | 通知有効化 | 許可後の表示変更 | × |
| 3 | 通知無効化 | 正常無効化 | × |
| 4 | リマインダー送信 | 前日通知 | × |
プッシュ通知のテストが最も多くの問題を抱えていました。
❌ No.2 通知有効化:許可後のボタン表示が切り替わらない
「通知を有効にする」「通知を無効にする」の両方が常に表示される設計になっていました。
解決方法: dashboard.blade.php にページ読み込み時のボタン表示切り替え処理(updateButtonState関数)を追加。
❌ No.3 通知無効化:ボタンが見えない
「通知を無効にする」ボタンの背景色(bg-gray-400)が白い背景と同化して見えなくなっていました。マウスでなぞると👆マークに変わることで存在には気づきましたが、視認できない状態でした。
解決方法: ボタンの色を bg-red-500(赤)に変更。
❌ No.4 リマインダー送信:複数のエラーが連鎖
Dockerを再ビルドした際、複数のエラーが連鎖して発生しました。
| エラー | 原因 | 解決方法 |
|---|---|---|
| 403 Forbidden | Apacheの権限設定不足 | DockerfileにAllowOverride Allを追加 |
| BCMath未インストール | プッシュ通知の暗号化に必要な拡張機能がない | Dockerfileにbcmathを追加して再ビルド |
| Dockerビルドのシンボリックリンクエラー |
public/storageがDockerのビルド時にコピーできない |
.dockerignoreにpublic/storageを追加 |
| Invalid data provided |
.envのVAPID_PRIVATE_KEY末尾に余分な文字が混入していた |
末尾の不要な文字を削除 |
最後のエラーは、.env ファイルを編集中に誤って別ファイルの名前が混入してしまったものでした。エラーメッセージだけでは原因の特定が難しく、.env の中身を一行ずつ確認することで見つかりました。
全エラーを解消後 → プッシュ通知が届きました!🎉
バリデーションエラーの日本語化
テスト全体を通じて、バリデーションエラーが英語または英語混じりで表示されるケースが複数の機能で見られました。バリデーション自体は正しく動作していたため各テスト項目の結果は○としましたが、ユーザー向けの表示として不適切なため、まとめて日本語化の対応を行いました。
解決方法:
-
lang/ja/validation.phpを作成 -
.envのAPP_LOCALE=jaに変更 -
config/app.phpの'locale' => 'ja'に変更 - キャッシュをクリア
php artisan config:clear
php artisan cache:clear
キャッシュクリアとは?
アプリが一時的に保存していた古い設定情報を削除して、最新の設定を読み込ませる操作です。設定ファイルを変更しても、キャッシュが残っていると変更が反映されないことがあります。
→ 「ニックネームは必須です。」「接種日は必須です。」など、全機能のエラーメッセージが日本語で表示されるようになりました。
テスト全体を通じて気づいたこと
項目に沿ってテストを進める中で、これまで気づいていなかった不具合や修正が必要な箇所が見つかりました。
機能としてはシンプルなアプリでも、項目ごとに分けて確認していくと想像以上にチェック箇所が多く、これまで「完成したつもり」になっていた点を反省しています。
特に印象に残ったのは以下の点です。
-
バグと仕様の違いを意識するようになった
「動かない=バグ」ではなく、設計の意図を確認することが大切だとわかりました。 -
テストしなければ気づかなかった不具合が多かった
ボタンが見えない、エラーメッセージが英語、重複登録できてしまう——どれも「動く」には動くのに、使う人には問題になる不具合でした。 -
ダミーファイルの作成方法やgif形式など、テストを通じて初めて知ったことがあった
テストは単に動作確認するだけでなく、知識を広げる機会でもありました。 -
エラーメッセージは手がかり
エラーメッセージを読んで原因を調べる習慣がつきました。最初は意味がわからなくても、キーワードで検索することで解決の糸口が見つかります。
今後の課題
テスト実施後、講師の方から以下のアドバイスをいただきました。
テストの粒度をより細かくする
例)名前・メール・パスワードをまとめて入力するのではなく、「名前のみ」「メールのみ」「パスワードのみ」など、項目ごとに分けてテストすることで、どの入力がNGなのかより細かく把握できます。
期待結果をより具体的に書く
例)「エラーメッセージが表示される」→「"〇〇は必須です。"というエラーメッセージが表示される」
自分以外の人がテストする場合にも役立ちます。規模の大きな開発ではテスト専属チームが担当することもあります。
入力値のパターンをさらに細かくする
例)名前の入力欄に「全角スペース」「数字」「絵文字」を入れた場合どうなるか、など。予期せぬエラーを防ぐことができます。
非機能要件のテストも含める
例)画面遷移時や保存時に1秒以上かからないこと、など。処理が遅い場合はデータ取得に時間がかかっている可能性があるため、インデックスの設定などで対策できます。
これらのアドバイスをもとに、今後はより質の高いテストケースを作成していきたいと思います。
まとめ
| 機能 | テスト項目数 | 不具合数 |
|---|---|---|
| 認証機能 | 8 | 1 |
| 子ども管理 | 7 | 1 |
| 接種スケジュール | 5 | 1 |
| 副反応記録 | 4 | 0 |
| 医療機関管理 | 5 | 0 |
| 予約管理 | 4 | 0 |
| 家族共有 | 6 | 2 |
| 母子手帳写真 | 5 | 0 |
| プッシュ通知 | 4 | 3+連鎖エラー4件 |
テストケースについて知り、実際に作成して実施したことで、複数の不具合を発見・修正するところまでたどり着けました。
テストは「完成したアプリをさらに良くするための工程」です。
「動いているから大丈夫」ではなく、「テストして初めて完成に近づく」という意識を持てるようになりました。
次のステップとして、手動テストだけでなく自動テスト(PHPUnitなど)やテストコードについても学んでいきたいと思います。
最後まで読んでいただきありがとうございました🙏