LaravelでPHPUnitで自動テストを行う

個人メモ。

# 環境
- Laravel 10.6.2
- PHP 8.2.0
- Vagrant CentOS7

# 準備

- PHPUnitの存在を確認しておきます。
Laravelルートで、下記のように出てくればOK。
```bash
$ vendor/bin/phpunit --version
PHPUnit 10.0.19 by Sebastian Bergmann and contributors.
```
動作確認。
laravelルートに移動して、デフォルトで置いてある
tests/Unit/ExampleTest.php
を動かしてみる。
```bash
$ vendor/bin/phpunit tests/Feature/ExampleTest.php
PHPUnit 10.0.19 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.0
Configuration: E:\vagrant\Vagrant2304\centos74\Laravelapp\laravelapp\phpunit.xml
.                                                                   1 / 1 (100%)
Time: 00:02.099, Memory: 24.00 MB
OK (1 test, 1 assertion)
```
動作OK。これは、これは、ルート/にアクセスして、存在あり：ステータス200を返すかどうかのテスト。
↓
\ExampleTest.php　の中身の一部
```PHP:ExampleTest.php
    public function test_the_application_returns_a_successful_response(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
```


- phpunit.xml
phpunit.xmlは、テストの環境設定ファイルで、デフォルトでは
./tests/Unit
./tests/Feature
フォルダのテストを行うようになっているが、今回は単独ファイル1つのみを指定してテストを行う予定なので、このファイルは修正しない。

- 使用するDBを複製し、テスト用DBを作成しておきます。
（データの削除、変更などを行うため、専用のテスト用DBをmigrateなどを使用して作成しておく）

- Laravelルートの.envファイルをコピーし、.env.testingファイルを作ります。これはテスト実行時の環境設定ファイルになります。

[テスト: テストの準備 10.x Laravel](https://readouble.com/laravel/10.x/ja/testing.html#creating-tests)
>プロジェクトのルートに.env.testingファイルを作成することもできます。このファイルは、PHPUnitテストを実行するとき、または--env=testingオプションを指定してArtisanコマンドを実行するときに、.envファイルの代わりに使用されます。

.env.testingを、テスト環境で使うデータベース設定に書き換えます。phpunit実行時には、この.env.testingの設定が使われます。
```:.env.testing
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
```

- ファクトリーの作成
テスト時にデータを追加するファクトリを作ります。
Userのダミーデータを作るfactoryはあらかじめあるので、今回はこれを使います。
\database\factories\UserFactory.php の中身は下記のようになっています。
```PHP:\database\factories\UserFactory.php
public function definition(): array
{
    return [
        'name' => fake()->name(),
        'email' => fake()->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
        'img' => 'noimage.jpg',
    ];
}
```
他のテーブルでもfactoryを使う場合には、`php artisan make:factory` コマンドで作成しておきます。
（今回は詳細は略）


- テストファイル作成
php artisan make:test コマンドでテストファイルを作成。今回はTodoTestという名前で作成。
※デフォルトでは、テストはtests/Featureディレクトリへ配置され、今回の場合には \tests\Feature\TodoTest.php　が作られます。
```bash
# php artisan make: test TodoTest
   INFO  Test [tests/Feature/TodoTest.php] created successfully.
```


- ページのステータス確認
では、作られたTodoTest.phpを編集して、`assertStatus`を呼び出す簡単なテストを作ってみます。
assertStatus　は、レスポンスに指定HTTPステータスコードがあることを確認するコマンドです。
参考：[HTTPテスト 10.x Laravel](https://readouble.com/laravel/10.x/ja/http-tests.html#assert-created)
ここで
`assertStatus(200)`は未ログイン状態でページが見えることを確認。
`assertStatus(302)`は未ログイン状態ではページが入れないことを確認。
```PHP:TodoTest.php
namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Todo;
use Illuminate\Support\Facades\Hash;

// ページのステータス確認
public function test_todo_status(): void
{
    //ログインしなくても見れる確認
    $response = $this->get('/todo');
    $response->assertStatus(200);   
    $response = $this->get('/todo/signup');
    $response->assertStatus(200);   
    $response = $this->get('/todo/login');
    $response->assertStatus(200);   
    $response = $this->get('todo/mypage_resign_logout');
    $response->assertStatus(200);   
    $response = $this->get('todo/forget_password');
    $response->assertStatus(200);   

    //ログインしないと入れない状態である確認
    $response = $this->get('/todo/logout');
    $response->assertStatus(302);   
    $response = $this->get('/todo_detail');
    $response->assertStatus(302);   
    $response = $this->get('/todo/new');
    $response->assertStatus(302);   
    $response = $this->get('/todo/edit');
    $response->assertStatus(302);   
    $response = $this->get('/todo/del');
    $response->assertStatus(302);   
    $response = $this->get('/todo/mypage');
    $response->assertStatus(302);   
    $response = $this->get('/todo/mypage_pass');
    $response->assertStatus(302);   
    $response = $this->get('/todo/mypage_resign');
    $response->assertStatus(302);   

}
```

テストファイルを作成したら、実行は、`php artisan test`コマンドに続けてファイル名を指定することで、テストが呼び出される。
```bash
#  php artisan test tests/Feature/TodoTest.php

   PASS  Tests\Feature\TodoTest
  ✓ todo status     　　                      0.12s  
  Tests:    1 passed (13 assertions)
  Duration: 0.21s

```
このようにテストが成功した。
万が一エラーが出た場合には、コードを修正していく。

続けてテストを記載していく。

- マイページが表示できるかのテスト
```PHP:TodoTest.php
// マイページが表示できるか
public function test_todo_mypage_show(): void
{
    // ユーザーを1人作成し、そのユーザーが　マイページを開いて、ステータスが200（無事表示できた）ことを確認
    $user = User::factory()->create();
    $response =  $this->actingAs($user)->get('/todo/mypage');
    $response->assertStatus(200);

    // テストデータ削除
    User::where('id', $user->id)->delete();
}
```
User::factory()->create(); でユーザーを1人作成。
そのユーザーが、ログインが必要なマイページにアクセスできるかを、`assertStatus(200)`でテスト。
今回、毎回最後に
User::where('id', $user->id)->delete();
で毎回該当のユーザーデータを削除している。
※テストファイル上部に
`use RefreshDatabase;`
と入れておくと、毎回データを削除してくれるらしいのだが、既存のデータもすべて消えるとのことで、今回は追加したデータのみ、その都度削除している。
参考：[データベースにテストデータが保存されるのを避ける方法](https://biz.addisteria.com/laravel_and_phpunit2/#toc6)

- データ追加テスト   
```PHP:TodoTest.php
// todo追加
public function test_can_new_todo(): void
{
    $user = User::factory()->create();

    $response =  $this->actingAs($user)->get('/todo/new');
    $response->assertStatus(200);

    $response = $this->actingAs($user)->post('/todo/new', [
        'title' => 'テスト投稿',
        'todo' => 'テスト投稿をします',
    ]);

    $this->assertDatabaseHas('todo', [
        'title' => 'テスト投稿',
        'todo' => 'テスト投稿をします',
        'user_id' => $user->id,
    ]);        

    // テストデータ削除
    Todo::where('user_id', $user->id)->delete();
    User::where('id', $user->id)->delete();
}
```
これは、`User::factory()->create()`でユーザーを作成、
`assertStatus(200)`でページが表示されることを確認。
`$this->actingAs($user)->post`でtodoテーブル（ユーザーテーブル1に対し多のリレーション関係にある）にデータ追加、
`$this->assertDatabaseHas`でtodoテーブルにデータ追加されているかの確認。
最後に、今回追加したテストデータ削除。

- データ編集テスト
```PHP:TodoTest.php
// todo編集
public function test_can_edit_todo(): void
{
    $user = User::factory()->create();

    // 投稿を作成
    $response = $this->actingAs($user)->post('/todo/new', [
        'title' => 'テスト投稿',
        'todo' => 'テスト投稿をします',
    ]);

    // 投稿を変更
    $todo = Todo::where('user_id', $user->id)->first();
    $todo_id = $todo->id;
    $response = $this->actingAs($user)->post('/todo/edit?id=' . $todo_id, [
        'title' => 'テスト投稿　変更',
        'todo' => 'テスト投稿を変更します',
    ]);
    // 変更したデータが存在することを確認
    $this->assertDatabaseHas('todo', [
        'title' => 'テスト投稿　変更',
        'todo' => 'テスト投稿を変更します',
        'user_id'=> $user->id,
    ]);

    // テストデータ削除
    Todo::where('user_id', $user->id)->delete();
    User::where('id', $user->id)->delete();
}
```
これは、`User::factory()->create()`でユーザーを作成、
まずデータを追加後、追加したデータを変更。
`$this->assertDatabaseHas`でtodoテーブルのデータが変更されているか確認。
最後に、今回追加・変更したテストデータ削除。


- データ削除
```PHP:TodoTest.php
// todo削除
public function test_can_del_todo(): void
{
    $user = User::factory()->create();

    // 投稿を作成
    $response = $this->actingAs($user)->post('/todo/new', [
        'title' => 'テスト投稿',
        'todo' => 'テスト投稿をします',
    ]);

    // 投稿を削除
    Todo::where('user_id', $user->id)->delete();
    // 変更したデータが存在しないことを確認
    $this->assertDatabaseMissing('todo', [
        'title' => 'テスト投稿',
        'todo' => 'テスト投稿をします',
        'user_id'=> $user->id,
    ]);

    // テストデータ削除
    Todo::where('user_id', $user->id)->delete();
    User::where('id', $user->id)->delete();
}
```
これは、`User::factory()->create()`でユーザーを作成、
まずデータを追加後、追加したデータを削除。
`$this->assertDatabaseMissing`でtodoテーブルに該当データが存在しないことを確認。
最後に、今回追加したテストデータ削除。

- ログアウト
```PHP:TodoTest.php
// todoログアウト
public function test_todo_logout(): void
{

    // 未ログイン状態でマイページにアクセスできない
    $response =  $this->get('/todo/mypage');
    $response->assertStatus(302);

    // ユーザーを1人作成し、そのユーザーがマイページを開いて、ステータスが200（無事表示できた）ことを確認
    $user = User::factory()->create();
    $response =  $this->actingAs($user)->get('/todo/mypage');
    $response->assertStatus(200);
    // ユーザーが認証済み
    $this->assertAuthenticatedAs($user);

    // ログアウト後に再度マイページを表示して、表示されないことを確認
    $response =  $this->actingAs($user)->get('/todo/logout');
    // ログインページにリダイレクト
    $response->assertStatus(302);
    $response->assertRedirect('/todo');
    // ログアウト後に認証されていないことを確認
    $this->assertGuest();

    // テストデータ削除
    User::where('id', $user->id)->delete();

}
```
これは、未ログイン状態でマイページにアクセスできないことを確認、`User::factory()->create()`でユーザーを作成し、マイページを開いて表示できることを確認。
ユーザーが認証済であることを`assertAuthenticatedAs`で確認。
次に、ログアウト後に再度マイページを表示して、表示されないことを確認。ログアウト後に認証されていないことを`assertGuest`で確認。
最後に、テストデータ削除。

- マイページ、ユーザー情報変更
```PHP:TodoTest.php
// マイページ　ユーザー情報変更
public function test_todo_mypage_change(): void
{
    // ユーザー作成
    $user = User::factory()->create();

    // 投稿を変更(name,email)
    $user_name = $user->name;
    $response = $this->actingAs($user)->post('/todo/mypage', [
        'name' => $user_name . '★',
        'email' => 'testuser@gmail.com',
    ]);
    $response->assertStatus(200);

    // 変更したデータが存在することを確認
    $this->assertDatabaseHas('users', [
        'id'=> $user->id,
        'name'=> $user_name . '★',
        'email' => 'testuser@gmail.com',
    ]);
    // テストデータ削除
    User::where('id', $user->id)->delete();
}

```
これは、ユーザーを作成し、ユーザーのnameを変更、その後変更したデータが存在することを`assertDatabaseHas`で確認、その後テストデータ削除。

- パスワード変更
```PHP:TodoTest.php
// パスワード変更
public function test_todo_password_edit(): void
{
    // ユーザー作成
    $user = User::factory()->create();

    // 投稿を変更
    $password = '12345678'; //変更後パス
    $response = $this->actingAs($user)->post('/todo/mypage_pass', [
        'current_password' => 'password',   //factoryで生成するpass
        'user_password' => $password,
        'confirm_password' => $password,
    ]);
    $response->assertStatus(200);
    
    $response->assertSeeText('更新しました。');

    // ユーザー情報を変更したのでrefresh()を使って値を更新する
    $user->refresh();
    // 変更したパスワードがユーザーのパスワードと一致しているか確認
    $this->assertTrue(Hash::check($password, $user->password));

    // テストデータ削除
    User::where('id', $user->id)->delete();

}
```
これは、ユーザーを作成し、ユーザーのパスワードを変更。
その後、変更したパスワードが現在のユーザーのパスワードと一致しているか確認。


- パスワード変更　5文字以上でなければエラー
```PHP:TodoTest.php
// パスワード変更　5文字以上でなければエラー
public function test_todo_password_edit_5char(): void
{
    // ユーザー作成
    $user = User::factory()->create();

    // 投稿を変更
    $password = '1111'; //変更後パス
    $response = $this->actingAs($user)->post('/todo/mypage_pass', [
        'current_password' => 'password',   //factoryで生成するpass
        'user_password' => $password,
        'confirm_password' => $password,
    ]);
    
    $response->assertSessionHasErrorsIn("新しいパスワードは、5文字以上で入力してください。");

    // テストデータ削除
    User::where('id', $user->id)->delete();

}
```
これは、ユーザー作成し、バリデーションが5文字以上でなければエラーのところ、4文字でパスワードを更新しようとします。
`assertSessionHasErrorsIn`で求めるエラー文が含まれているか確認します。


- 最後に動作確認
すべてのテストを実行してみます。途中でエラーが起こったところもあるのですが、ソースを修正することで、テストがすべて通過するようになりました。
```bash
# php artisan test tests/Feature/TodoTest.php

   PASS  Tests\Feature\TodoTest
  ✓ todo status                          1.36s
  ✓ todo mypage show                     0.37s
  ✓ can new todo                         0.30s
  ✓ can edit todo                        0.21s
  ✓ can del todo                         0.15s
  ✓ todo logout                          0.19s
  ✓ todo mypage change                   0.40s
  ✓ todo password edit                   0.32s
  ✓ todo password edit 5char             0.16s

  Tests:    9 passed (35 assertions)
  Duration: 3.93s
```


# 参考

[テスト: テストの準備 10.x Laravel](https://readouble.com/laravel/10.x/ja/testing.html#creating-tests)
[データベーステスト 10.x Laravel](https://readouble.com/laravel/10.x/ja/database-testing.html)
[HTTPテスト 10.x Laravel](https://readouble.com/laravel/10.x/ja/http-tests.html#assert-created)

[LaravelでPHPUnitを使ってCRUD処理のテストを自動化①【投稿の作成と保存テスト】](https://biz.addisteria.com/laravel_and_phpunit2/)
[LaravelでPHPUnitを使ってテストする手法](https://qiita.com/shindex/items/4f28f8e06ef2d10e8d2b#%E8%BF%BD%E8%A8%98)
[Laravelでログイン関係のテストをしてみる](https://tenrakatsuno.com/programing-note/laravel-login/)
[LaravelでEmailとPassword変更についてのテストを書いてみた](https://tenrakatsuno.com/programing-note/laravel-email-password-test/)