4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravelのユニットテストで詰まった話

Last updated at Posted at 2022-03-28

はじめに

タイトルにある通り未解決ですが、 Laravelでユニットテストをコーディングしていた時に起こった事を書きます。
テストクラスにはメソッドが2つあり、1つずつテストを実行した時は問題なくそれぞれPassします。
しかし、通しで2つ同時に実行するとこけちゃいます。

環境

$php --version
PHP 8.0.16 (cli) (built: Feb 18 2022 20:51:37) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.16, Copyright (c) Zend Technologies

$php artisan --version
Laravel Framework 8.36.2

$phpunit --version
PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

$mysql --version
mysql  Ver 14.14 Distrib 5.6.38, for osx10.9 (x86_64) using  EditLine wrapper

テスト準備

テスト用のDBでテストするための準備を簡単に書きます。

.env.testing
APP_ENV=testing
DB_CONNECTION=testing
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=root
DB_PASSWORD=root
phpunut.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="sqlite"/> -->
    <!-- <server name="DB_DATABASE" value=":memory:"/> -->
    <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>
migration_create_clients.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTblClient extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        // テストに関係ないテーブル定義は簡略しています。
        Schema::create('clients', function (Blueprint $table) {
            $table->increments('id');
            $table->string('client_id');
            $table->string('type');
            $table->string('name');
            $table->integer('test_flag');
            $table->timestamp('reg_date');
            $table->timestamp('upd_date');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('clients');
    }
}
ClientSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Client;
use Illuminate\Database\Seeder;

class ClientSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Client::insert([
            [
//                'id' => 1,
                'client_id' => 'hoge',
                'type' => 'super',
                'name' => 'ほげ',
                'test_flag' => 0,
            ],
            [
//                'id' => 2,
                'client_id' => 'hogehoge',
                'type' => 'super',
                'name' => 'ほげほげ',
                'test_flag' => 0,
            ],
            [
//                'id' => 3,
                'client_id' => 'fuga',
                'type' => 'super',
                'name' => 'ふが',
                'test_flag' => 0,
            ],
            [
//                'id' => 4,
                'client_id' => 'fugafuga',
                'type' => 'super',
                'name' => 'ふがふが',
                'test_flag' => 0,
            ],
            [
//                'id' => 5,
                'client_id' => 'hogefuga',
                'type' => 'super',
                'name' => 'ほげふが',
                'test_flag' => 1,
            ],
        ]);
    }
}
DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            ClientSeeder::class,
        ]);
    }
}
ClientTest.php
<?php

namespace Tests\Unit;

use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ClientTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();
        $this->artisan('db:seed');
        $this->client = $this->app->make('App\略\Client');
    }

    public function test_fetchNonTestClientType()
    {
        $type = 'super';
        $search = 'h';
        $res = $this->client->fetchNonTestClientType($type, $search);
        $this->assertEquals([
            [
                'id' => 1,
                'client_id' => 'hoge',
                'name' => 'ほげ',
            ],
            [
                'id' => 2,
                'client_id' => 'hogehoge',
                'name' => 'ほげほげ',
            ],
        ], $res);

        $search = 'ふが';
        $res = $this->client->fetchNonTestClientType($type, $search);
        $this->assertEquals([
            [
                'id' => 3,
                'client_id' => 'fuga',
                'name' => 'ふが',
            ],
            [
                'id' => 4,
                'client_id' => 'fugafuga',
                'name' => 'ふがふが',
            ],
        ], $res);

        $search = '';
        $res = $this->client->fetchNonTestClientType($type, $search);
        $this->assertEquals([
            [
                'id' => 1,
                'client_id' => 'hoge',
                'name' => 'ほげ',
            ],
            [
                'id' => 2,
                'client_id' => 'hogehoge',
                'name' => 'ほげほげ',
            ],
            [
                'id' => 3,
                'client_id' => 'fuga',
                'name' => 'ふが',
            ],
            [
                'id' => 4,
                'client_id' => 'fugafuga',
                'name' => 'ふがふが',
            ],
        ], $res);

        $search = 'dahodajdadaoi';
        $res = $this->client->fetchNonTestClientType($type, $search);
        $this->assertEquals([], $res);

        $type = 'ultra';
        $search = 'h';
        $res = $this->client->fetchNonTestClientType($type, $search);
        $this->assertEquals([], $res);
    }

    public function test_setTestFlag()
    {
        $res = $this->client->setTestFlag(1);
        $this->assertTrue($res);
        $res = $this->client->setTestFlag(5);
        $this->assertTrue($res);

        $this->expectException(Exception::class);
        $this->client->setTestFlag(10);
    }
}

Client.php
<?php

namespace App\略\Client;

use Exception;
use App\Models\Client;
use Throwable;

class Client
{
    public function __construct(private $client = new Client){}

    /**
     *
     * @param string $type
     * @param string $search
     * @return array
     */
    public function fetchNonTestClientType(string $type, string $search): array
    {
        try {
            $res = $this->clientModel->select('id', 'client_id', 'name')
                ->where('type', $type)
                ->where(function($query) use ($search){
                    $query->where('client_id', 'like', "%$search%")
                        ->orWhere('name', 'like', "%$search%");
                })
                ->where('test_flag', '<>', 1)
                ->get()
                ->toArray();
        } catch (Throwable $e) {
            throw new Exception($e, 500);
        }
        return $res;
    }

    /**
     *
     * @param integer $id
     * @return boolean
     */
    public function setTestFlag(int $id): bool
    {
        try {
            $client = $this->client->findOrFail($id);
            $client->test_flag = 1;
            $res = $client->save();
        } catch (Throwable $e) {
            throw new Exception($e, 500);
        }
        return $res;
    }

長々とファイルを書きましたが簡単に説明すると

  1. fetchNonTestClientType()Clientから条件にあったレコードを取得してtoArray()して返す。
  2. setTestFlag()は渡された引数のidのtest_flagを1書き換えてsave()して実行結果を返す。
    という事を行なっています。
  3. test_fetchNonTestClientType()$type$searchを投げて検索結果が想定通りのものか確認するテスト
  4. test_setTestFlag()は指定した$idのレコードが上書きに成功したか、$idが見つからずExceptionを発行したかどうかを確認するテスト

となっています。

テスト実行

やっとテスト実行の準備ができました。
まずはテストを個別に実行します。

$ php artisan test tests/Unit/ClientTest.php --filter test_fetchNonTestClientType

   PASS  Tests\Unit\ClientTest
  ✓ fetch non test client type

  Tests:  1 passed
  Time:   0.91s

$ php artisan test tests/Unit/ClientTest.php --filter test_setTestFlag

   PASS  Tests\Unit\ClientTest
  ✓ set test flag

  Tests:  1 passed
  Time:   0.89s

うんうん、いい感じ。

それでは本命のClientTest.phpをまとめて実行してみます。

$ php artisan test tests/Unit/ClientTest.php

   FAIL  Tests\Unit\ClientTest
  ✓ fetch test client type
  ⨯ set test flag

  ---

  • Tests\Unit\ClientTest > set test flag
   Exception

  Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\Client] 1 in /var/www/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:420
Stack trace:
#0 /var/www/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php(23): Illuminate\Database\Eloquent\Builder->findOrFail(1)
#1 /var/www/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(1890): Illuminate\Database\Eloquent\Model->forwardCallTo(Object(Illuminate\Database\Eloquent\Builder), 'findOrFail', Array)
#2 /var/www/app/略/Client.php(361): Illuminate\Database\Eloquent\Model->__call('findOrFail', Array)
#3 /var/www/tests/Unit/ClientTest.php(91): App\略\Client->setTestFlag(1)
#4 /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php(1527): Tests\Unit\ClientTest->test_setTestFlag()
#5 /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php(1133): PHPUnit\Framework\TestCase->runTest()
#6 /var/www/vendor/phpunit/phpunit/src/Framework/TestResult.php(722): PHPUnit\Framework\TestCase->runBare()
#7 /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php(885): PHPUnit\Framework\TestResult->run(Object(Tests\Unit\ClientTest))
#8 /var/www/vendor/phpunit/phpunit/src/Framework/TestSuite.php(678): PHPUnit\Framework\TestCase->run(Object(PHPUnit\Framework\TestResult))
#9 /var/www/vendor/phpunit/phpunit/src/TextUI/TestRunner.php(670): PHPUnit\Framework\TestSuite->run(Object(PHPUnit\Framework\TestResult))
#10 /var/www/vendor/phpunit/phpunit/src/TextUI/Command.php(143): PHPUnit\TextUI\TestRunner->run(Object(PHPUnit\Framework\TestSuite), Array, Array, true)
#11 /var/www/vendor/phpunit/phpunit/src/TextUI/Command.php(96): PHPUnit\TextUI\Command->run(Array, true)
#12 /var/www/vendor/phpunit/phpunit/phpunit(92): PHPUnit\TextUI\Command::main()
#13 {main}

  at app/略/Client/Client.php:365
    361▕             $client = $this->client->findOrFail($id);
    362▕             $client->test_flag = 1;
    363▕             $res = $client->save();
    364▕         } catch (Throwable $e) {
  ➜ 365▕             throw new Exception($e, 500);
    366▕         }
    367▕         return $res;
    368▕     }
    369▕ }

  1   tests/Unit/ClientTest.php:91
      App\略\Client::setTestFlag()


  Tests:  1 failed, 1 passed
  Time:   1.04s

エラーを見る限りtest_setTestFlag$this->expectException(Exception::class);を宣言?する前にExceptionが発行されてしまっている様です。個別のテストでうまくいってるのになんだこれと思って色々試してみました。

  1. test_fetchNonTestClientType()test_setTestFlag()の順序を変えてみた。
  2. primaryKeyを指定しながらseedしてみた。(seederのコメントアウトを解除)
  3. test_setTestFlag()の先頭で$this->expectException(Exception::class);を追加
 // 1の結果
$ php artisan test tests/Unit/ClientTest.php

   FAIL  Tests\Unit\ClientTest
  ✓ set test flag
  ⨯ fetch test client type

  ---

  • Tests\Unit\ClientTest > fetch test client type
  Failed asserting that two arrays are equal.

  at tests/Unit/ClientTest.php:40
     36▕         $search = 'h';
     37▕         $res = $this->client->fetchNonTestClientType($type, $search);
     38▕         $this->assertEquals([
     39▕             [
  ➜  40▕                 'id' => 1,
     41▕                 'client_id' => 'hoge',
     42▕                 'name' => 'ほげ',
     43▕             ],
     44▕             [
  --- Expected
  +++ Actual
  @@ @@
   Array (
       0 => Array (
  -        'id' => 1
  +        'id' => 6
           'client_id' => 'hoge'
           'name' => 'ほげ'
       )
       1 => Array (
  -        'id' => 2
  +        'id' => 7
           'client_id' => 'hogehoge'
           'name' => 'ほげほげ'
       )
   )

  Tests:  1 failed, 1 passed
  Time:   1.01s


// 2の結果
$ php artisan test tests/Unit/ClientTest.php

   PASS  Tests\Unit\ClientTest
  ✓ fetch fetch test client type
  ✓ set test flag

  Tests:  2 passed
  Time:   1.00s


// 3の結果
$ php artisan test tests/Unit/ClientTest.php

   PASS  Tests\Unit\ClientTest
  ✓ fetch fetch test client type
  ✓ set test flag

  Tests:  2 passed
  Time:   1.00s

それぞれの結果を見ると、auto incrementしているidが違っていることによりエラーになっていたことがわかりました。
これらを見るに今のテストの設定だと1つのテストメソッド毎にseedされている?という疑問が浮上したのでテストクラスに1つ実験用メソッドを追加しました。

ClientTest.php
public function test_hoge()
{
    $type = 'sf';
    $search = 'h';
    $res = $this->client->fetchNonTestClientType($type, $search);
    $this->assertEquals([
        [
            'id' => 1,
            'client_id' => 'hoge',
            'name' => 'ほげ',
        ],
        [
            'id' => 2,
            'client_id' => 'hogehoge',
            'name' => 'ほげほげ',
        ],
    ], $res);
}
php artisan test tests/Unit/ClientTest.php

   FAIL  Tests\Unit\ClientTest
  ✓ fetch test client type
  ✓ set test flag  // 先頭でExceptionを許容
  ⨯ hoge

  ---

  • Tests\Unit\ClientTest > hoge
  Failed asserting that two arrays are equal.

  at tests/Unit/ClientTest.php:109
    105▕         $search = 'h';
    106▕         $res = $this->client->fetchNonTestClientType($type, $search);
    107▕         $this->assertEquals([
    108▕             [
  ➜ 109▕                 'id' => 1,
    110▕                 'client_id' => 'hoge',
    111▕                 'name' => 'ほげ',
    112▕             ],
    113▕             [
  --- Expected
  +++ Actual
  @@ @@
   Array (
       0 => Array (
  -        'id' => 1
  +        'id' => 11
           'client_id' => 'hoge'
           'name' => 'ほげ'
       )
       1 => Array (
  -        'id' => 2
  +        'id' => 12
           'client_id' => 'hogehoge'
           'name' => 'ほげほげ'
       )
   )

  Tests:  1 failed, 2 passed
  Time:   1.13s

予想通りテストメソッド毎にseedされているようです

さいごに

setUp()RefreshDatabaseが全てのテストメソッド実行前ではなくテスト実行前にだけ実行されると思い込んでいたことが原因でこういった事象が起こりました。今回の解決案としては

  1. seederを書く時はautoincrements指定されているカラムもきちんと書いてやる
  2. seedするのはartisanコマンドで事前にやってもらい、RefreshDatabaseを指定しないことで採番が変わらない様にする。

今回は1を採用することにしました。

余談

ほんとは未解決ってタイトルにしようと思ってましたが、前日寝ようとしてる時にふと思いついて解決してしまいました。(よくあるよね)

4
0
1

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
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?