LoginSignup
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-13

はじめに

Laravelの環境は以下。

$ php artisan --version

Laravel Framework 8.39.0

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

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

参考

公式ドキュメント(Laravel8系)

データベーステスト
HTTPテスト

テスト用DB準備

DB作成

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

mysql> create database yamato_test;

設定ファイル準備

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

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


'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タグの中身を変えます。

 <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

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

  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.phpprovidersに以下を追記。

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

場所はどこでもOK。

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

$ php artisan iseed User

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

<?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を用いて任意の値を生成する。


    /**
     * 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配下にファイル準備する。
FeatureUnitディレクトリの大きな違いは無い。
プロジェクトの方針による。

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

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

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

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

アサーション

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

アサーションメソッドの種類については以下にあります。

PHPUNITについて
このほかにもたくさんのメソッドがあります。

テスト実行

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

$ vendor/bin/phpunit

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

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

テーブル初期化

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

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

<?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

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

ファクトリーの使用

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へ保存せず、インスタンスを使用することもできる。

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

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を取得する方がほかのテストの影響がなくなる。

モデルのテスト

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

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

    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()で確認。

バリデーション確認

 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()で作成。

    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配下(任意の場所)に配置。

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、第二引数にパラメーターを配列で指定する。

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

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

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

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

$query = ["selected_user_id" => $this->me_user->id,];
$response = $this->ActingAs($this->admin_user)->call('GET', '/get-payment-dates', $query);

パラメーターを配列で作成し、call()の第三引数で渡すとリクエストを作成できる。

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

返り値確認の例

// 想定した文字列か
$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()を使用する。

書き方は以下。

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

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

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
What you can do with signing up
3