PHPUNIT ユニットテストの手順および学び

# はじめに

Laravelの環境は以下。

```
$ php artisan --version

Laravel Framework 8.39.0
```

テストコード作成にてこずりましたので
その時の学びをまとめました。
特につまずいたのが以下。

・テスト用DB設定
・JSON型カラムの大量データをシーディング
・テーブルのリフレッシュ
・事前データを設定
・あるユーザーとしてアクセス
・ファイルをアップロードしてアクセス
・GETリクエストでパラメーター送信

# 参考

公式ドキュメント(Laravel8系)
↓
[データベーステスト](https://readouble.com/laravel/8.x/ja/database-testing.html)
[HTTPテスト](https://readouble.com/laravel/8.x/ja/http-tests.html#assert-download)

# テスト用DB準備

#### DB作成
開発用DBとは別にテストDBを作成する。
例として【yamato_test】とする。

```
mysql> create database yamato_test;
```

#### 設定ファイル準備

**/config/database.php**に設定を用意する。
このファイルの中の**connections**に以下を追記する。

開発のMySQLとほぼ同じ情報で、DB名だけ変更する。
DB名は**env**ファイルで指定する。

```php

'connections' => [
    'mysql' => [
            // 省略
        ],

    'testing' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_TEST_DATABASE', 'forge'),//データベース名
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => false,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
            'timezone' => '+09:00',
        ],
```

.envファイルに以下追記。

```
DB_TEST_DATABASE=yamato_test
```

PHPUnitで設定したDBを使用するよう記述する。
**phpunit.xml**のphpタグの中身を変えます。

```xml
 <php>
    <server name="APP_ENV" value="testing"/>
    <server name="BCRYPT_ROUNDS" value="4"/>
    <server name="CACHE_DRIVER" value="array"/>
    <server name="DB_CONNECTION" value="testing"/>
    <server name="DB_DATABASE" value="minipo_test"/>
    <server name="MAIL_MAILER" value="array"/>
    <server name="QUEUE_CONNECTION" value="sync"/>
    <server name="SESSION_DRIVER" value="array"/>
    <server name="TELESCOPE_ENABLED" value="false"/>
  </php>
```

`<server name="DB_CONNECTION" value="testing"/>`で、 **/config/database.php** の`'testing'`の設定を反映させる。

接続先が同じでDB名が違うだけなら以下だけでよい（かも）
`<server name="DB_DATABASE" value="minipo_test"/>`

# ダミーデータ準備

ある程度開発後のテストのためマイグレーションファイルは作成済み前提です。
※後回しでも良い

#### テストDBにマイグレーションを適用

```
$ php artisan migrate:refresh --database=testing
```

`--database=testing`を忘れると開発DBがリフレッシュされていますので注意。

### シーデングの用意

**User**モデル用のシーデングファイル作成。

```
$ php artisan make:seeder UserSeeder
```

例えば内容は以下のように。(例)

```php
  public function run()
    {
        DB::table('users')->insert([
            'name'                  => 'admin',
            'email'                 => 'admin@gmail.com',
            'password'              => Hash::make('password'),
            // 中略
        ]);
    }
```

#### 自動でシーディング作成する方法
現在のテーブルの情報を元にシーデングファイルを作成する方法。
手書きでも問題はないが、今回は**json**型に大量のデータが入る場合のシーディングファイル作成はとても手間がかかるためこの方法を採用した。

**iseed**をインストール
サーバー上で以下のコマンド。

```
$ composer require --dev "orangehill\iseed"
```

**config/app.php**の**providers**に以下を追記。

```php
          /*
         * Package Service Providers...
         */
        Orangehill\Iseed\IseedServiceProvider::class,
```

場所はどこでもOK。

以下のコマンドでファイルが作成される。
例で`User`テーブルの場合。

```
$ php artisan iseed User
```

そうすると**database/seeders/UserTableSeeder**が作成される。

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class UserTableSeeder extends Seeder
{
    /**
     * Auto generated seed file
     *
     * @return void
     */
    public function run()
    {

        // \DB::table('salaries')->delete();
        
        DB::table('salaries')->insert(array (
            0 => 
            array (
                'created_at' => NULL,
                'name' => 'admin',
                // 中略
            ),
        ));
    }
```

ここで`delete()`は削除かコメントアウトします。
理由は個別にdeleteするとリレーション関連でうまく動作しないことがあるから。
全体でテーブルはリフレッシュされるので問題ない。

あとは必要な追記、削除して調整します。JSONカラムの大量データもしっかり対応できます。



# ファクトリを作成
※後回しでも良い。

シーディングとの違いは、50や100とか大量にデータ作成するときはコチラ。
シーディングは少量で指定のデータ作成に向く。

**User**モデル用のファクトリファイル作成。

```
$ php artisan make:factory UserFactory
```

内容は例えば以下のようになる。
`faker`を用いて任意の値を生成する。

```php

    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail;
            //中略
        ];
    }
```

# テストファイル

**/tests**配下にファイル準備する。
**Feature**と**Unit**ディレクトリの大きな違いは無い。
プロジェクトの方針による。

`TestCase`クラスを継承していればテストされる。

ここで`tests/Feature/ExampleTest.php`の中身を以下のようにする。

```php
    public function testBasicTest()
    {
        $response = $this->get('/');
        $response->assertStatus(302);//200→302に変更
    }
```

一旦デフォルトの状態でテストが通るか確認したいが、ミドルウェアで`Auth`を使用していると場合によっては別ページへリダイレクトされるためその対策。

#### アサーション

テストの記述は基本的に
`$this->(アサーションメソッド)`で記述します。

アサーションメソッドの種類については以下にあります。
↓
[PHPUNITについて](https://phpunit.readthedocs.io/ja/latest/assertions.html#)
このほかにもたくさんのメソッドがあります。

#### テスト実行

サーバー上のアプリのルートで以下のコマンドでテスト実行。

```
$ vendor/bin/phpunit
```

緑の文字でOKと出ればテスト成功。

テストが実行されるのは`TestCase`クラスを継承しているクラスで
その中の`test`から始まる`function`が実行されます。

# テーブル初期化
テスト毎に同じ環境を作ることが求められます。
そこでテストの度に自動でテーブルをリセットするように記述します。

ここでは`ExampleTest.php`を元にUserモデルテスト用のファイルを作成しました。
以下の場所に`use RefreshDatabase;`を追記。

```php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase; //この記述

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testUserModel()
    {
      // 中略
    }
}
```

`use RefreshDatabase;`と記述することで以下のコマンドを呼び出し
このクラスをテストするときにテーブルが初期化されるようになる。

```
$ php artisan migrate:refresh --database=testing
```

**各クラスごとこの記述は追記する。**
理由はなるべくクラス毎に疎結合となるように記述するべきだから。

### ファクトリーの使用

```php
use Illuminate\Database\Eloquent\Factories\Sequence;

//中略

    public function testUserModel()
    {

        // 10人のダミーデータ作成
        User::factory()->count(10)
                        ->state(new Sequence(
                            ['admin' => true],
                            ['admin' => false],
                        ))
                        ->create();//またはmake()
    }
```

このような記述で作成したファクトリファイルを元にタミーデータ作成する。
`count()`で数を指定。
`state(new Sequence())`で特定の値を作成できる。この場合は半々でデータ作成される。
`create()`でDBへ保存。
`make()`でDBへ保存せず、インスタンスを使用することもできる。

### テスト実行前に準備したい場合

```php
use Database\Seeders\SalarySeeder;
use Database\Seeders\UserSeeder;

class SalaryControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * テスト用データ作成
     *
     * @return void
     */
    protected function setUp(): void
    {
        parent::setUp();
        $this->admin_user = User::factory()->create(['admin' => true,]);
        $this->me_user = User::factory()->create(['admin' => false,]);
        $this->seed(UserSeeder::class);
        $this->seed(SalarySeeder::class);
    }
```

`setUp(): void`でそのクラスのテスト実行するまえにこの中身を実行する。
`parent::setUp();`この記述も必要。

#### 値の流用

ここで作成した変数を他で使用したい場合は`$admin_user`でなく`$this->admin_user`とすることで、他のファンクションでも値を呼び出せる。

#### シーディング実行

`$this->seed(UserSeeder::class);`これでシーディングファイルの内容を呼び出せる。
**リレーション関係ある場合は作成の順番に気を付けないと外部キーエラーとなる**
またシーディングファイルで親モデルidを指定する場合思い通りの数字ではない場合があるため、他のキーでオブジェクトを呼び出しidを取得する方がほかのテストの影響がなくなる。

## モデルのテスト

今回は以下の点をテストします。
・レコードが正常に作成されているか
・レコードが正常に削除できるか
・バリデーションがかかているか

まず正常に作成されているか確認します。

```php
    public function testUserModel()
    {

        // 10人のダミーデータ作成
        User::factory()->count(10)
                        ->state(new Sequence(
                            ['admin' => true],
                            ['admin' => false],
                        ))
                        ->create();
        
        // 10人登録完了してるか
        $count = User::get()->count();
        $this->assertEquals(10, $count);

        // 任意のユーザー確認
        $recode_first_id = User::first()->id;
        $recode_last_id = User::all()->last()->id;
        $user = User::find(rand($recode_first_id, $recode_last_id));
        
        $user_email = $user->email;
        $this->assertDatabaseHas('users', ['email' => $user_email,]);

        // 理論削除されてるか確認
        $user->delete();
        $this->assertDatabaseHas('users', ['email' => $user_email,]);
        $this->assertSoftDeleted($user);
        $delete_user = User::where('email', $user_email)->first();
        $this->assertEmpty($delete_user);
    }
```

ファクトリーを用いてデータ作成し、レコード数や任意のユーザーのデータを確認しています。
削除に関しては`SoftDelete`に対応しています。

`assertDatabaseHas(テーブル名, 連想配列)`はそのテーブルに配列のデータ（一部でもよい）を持つレコードが存在するかチェック。あれば`True`となる。

### 論理削除の場合

論理削除のときは注意が必要。
`assertDatabaseHas()`ではレコードとして認識されている。ただクエリではヒットしないことを`assertEmpty()`で確認。

#### バリデーション確認

```php
 public function testDenyUserRegister()
    {
        // 登録可能なサンプルデータ
        $sample_data = [
            'name'        => 'yamato',
            'email'       => 'yamato@yamato.com',
           // 中略
        ];
        $user = new User();
        $user->fill($sample_data)->save();
        $this->assertDatabaseHas('users', $sample_data);

        // nameのnull制限
        $name_null_data = array_replace($sample_data, ['name' => null]);
        $user = new User();
        try {
            $user->fill($name_null_data)->save();
        } catch ( \Exception $e ) {
        }
        $this->assertDatabaseMissing('users', $name_null_data);

}
```

一度正常なデータを登録したあと、制限かかるデータを作成し`assertDatabaseMissing()`で登録できていないことを確認している。
登録失敗で`try-catch`で処理が止まらないようにする。


## コントローラーのテスト

コントローラーのテストは以下のテスト。
・正常なアクセスができているか
・権限により許可、リダイレクトできているか
・想定通りのリターンがあるか

#### アクセス確認

ミドルウェアに**Auth**を使用しているとログインした状態でないと正常にアクセスができない。
その場合は`ActingAs()`で指定ユーザーとしてアクセスする。
アクセスは`$this->get()`や`$this->post()`で作成。

```php
    public function testSample()
    {
      // アクセス確認
      $this->get('/address/index')->assertStatus(302);
      $this->ActingAs($this->me_user)->get('/address/index')->assertOk();
      $this->ActingAs($this->admin_user)->get('/address/index')->assertOk();

      // スタッフでアクセス
      $response = $this->ActingAs($this->me_user)->get('/address/index');

      $this->assertSame('', $response['is_search']);

      // 管理者でアクセス
      $response = $this->ActingAs($this->admin_user)->get('/index');

      $this->assertNotSame('', $response['me_users']);
      $this->assertSame(null, $response['is_search']);
    }
```

`$this->me_user`や`$this->admin_user`は事前に権限が異なるユーザーを作成し呼び出している。

HTTPアクセスの確認は`$response`でアクセスを作成し、
それをテストする流れ。

`assertStatus(302)`は権限ないとリダイレクトされていることを確認。
`assertOk()`はステータス200で正常に通信できたことを確認。

`assertSame()`で返り値が想定の値か確認をする。

## ファイルをアップロードするアクセス

今回は`CSVファイル`を含むアクセスを作成する方法。
まずファイルを**storage/app/public/test_data**配下（任意の場所）に配置。

```php
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;

  // CSVファイル準備
  $path = Storage::path('public/test_data/test_user.csv');
  $file = new UploadedFile($path, 'test_user.csv', 'text/plain');
```

`UploadedFile`クラスを用いてアップロードファイルを作成する。
第一引数にパス、第二引数にファイル名、第三引数にファイルの種類を指定する。

それをパラメーターに指定してPOSTアクセスを作成する。
`post()`は第一引数にURI、第二引数にパラメーターを配列で指定する。

```php
  $response = $this->ActingAs($admin_user)->post('/csv-import-user', ['input_file' => $file]);
```

そして返り値をテストする。
正常ファイルで問題ないか確認。
あえてエラーが出るファイルでエラーが機能するか確認する。

## GETリクエストでパラメーター送る方法

`get()`ではパラメーターを送れなかったので`call()`を使用する。

```php
$query = ["selected_user_id" => $this->me_user->id,];
$response = $this->ActingAs($this->admin_user)->call('GET', '/get-payment-dates', $query);
```
パラメーターを配列で作成し、`call()`の第三引数で渡すとリクエストを作成できる。

ちなみに**API**の場合は`URI`を`/api/`をするだけでよい。

## 返り値確認の例

```php
// 想定した文字列か
$this->assertSame('yes', $response['is_search']);
$this->assertSame(null, $response['is_search']);

// エラーメッセージが配列に入っている場合
$errors = $response['errors'];
$this->assertContains('2行目、データの列数が多いです。', $errors);
$this->assertContains('3行目、データの列数が少ないです。', $errors);

// オブジェクトを取得できているか
$this->assertIsObject($response['users']);

// PDFダウンロードが正常かヘッダー情報で確認
$response->assertHeader('content-type', 'application/pdf');
$response->assertHeader('content-disposition', 'attachment; filename="サンプルPDF.pdf"');

// TEXTダウンロードが正常かヘッダー情報で確認
$response->assertHeader('content-type', 'text/plain; charset=UTF-8');
$response->assertHeader('content-disposition', 'attachment; filename="サンプルテキスト.txt"');
```
## 一部に該当文字列が含まれているか確認

 

`assertMatchesRegularExpression()`を使用する。

書き方は以下。


```php
$this->assertMatchesRegularExpression('/該当文字列を正規表現で表示/', $result);
```

その該当文字列に`スラッシュ`など記号が含まれエスケープが必要な場合は以下の対策をする。

https://qiita.com/yamato1491038/items/90f6e3695b3e867f96e9