はじめに
タイトルにある通り未解決ですが、 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でテストするための準備を簡単に書きます。
APP_ENV=testing
DB_CONNECTION=testing
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=root
DB_PASSWORD=root
<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>
<?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');
}
}
<?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,
],
]);
}
}
<?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,
]);
}
}
<?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);
}
}
<?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;
}
長々とファイルを書きましたが簡単に説明すると
-
fetchNonTestClientType()
はClient
から条件にあったレコードを取得してtoArray()
して返す。 -
setTestFlag()
は渡された引数のid
のtest_flagを1
書き換えてsave()
して実行結果を返す。
という事を行なっています。 -
test_fetchNonTestClientType()
は$type
と$search
を投げて検索結果が想定通りのものか確認するテスト -
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が発行されてしまっている様です。個別のテストでうまくいってるのになんだこれと思って色々試してみました。
-
test_fetchNonTestClientType()
とtest_setTestFlag()
の順序を変えてみた。 -
primaryKey
を指定しながらseedしてみた。(seederのコメントアウトを解除) -
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つ実験用メソッドを追加しました。
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
が全てのテストメソッド実行前ではなくテスト実行前にだけ実行されると思い込んでいたことが原因でこういった事象が起こりました。今回の解決案としては
- seederを書く時はautoincrements指定されているカラムもきちんと書いてやる
- seedするのはartisanコマンドで事前にやってもらい、RefreshDatabaseを指定しないことで採番が変わらない様にする。
今回は1を採用することにしました。
余談
ほんとは未解決ってタイトルにしようと思ってましたが、前日寝ようとしてる時にふと思いついて解決してしまいました。(よくあるよね)