こんにちは、ATです。
みなさん、テストしてますか!
新型コロナがまたぞろ世間を騒がしていますが、それとはまったく関係なく、Laravelでテストを書いた際に経験した、あれやこれやを書いていきます。
前提:Laravel 6.x PHPUnit
Httpテスト
PHPUnit
Laravelでは標準でPHPUnitが準備されています。 その為。何もやらなくても、
phpunit
を実行するだけでテストが行えます。
嘘です。 これだけだとコマンドが見つからん!と怒られます。 下記のように指定することでテストが実行されます。
プロジェクトのルートにいるていで
./vendor/bin/phpunit
(ドキュメントにはコマンドラインからphpunitを実行してとしか書いてない。また検索すると同じくphpunitとしか書いてないページもちらほらと、、、)
テストを書いて実行する前には念の為、config:clear Artisanコマンドを実行して設定キャッシュ(storage/cache/config.php)をクリア(削除)しときましょう。 意図しないDBが操作され、最悪データが消滅する可能性があります。私はこれのせいで、開発のDBに謎のユーザを大量生成しました。 (このせいでPRチェック中に謎エラーを出すハメに、、、)
.env.testing
Artisanコマンドを–env=testingオプション付きで実行すると、.envファイルを.env.testingファイルの内容でオーバーライドします。 ここで注意が必要なのはオーバーライドするだけ。ということです。 その為、–env=testingオプション付きで実行した際、.env.testingがなくてもエラーにならずテストを実行します。これも、意図しないDB操作につながる可能性があります。
Laravel test
testsディレクトリには、2つのディレクトリが存在しています。 Feature とUnitです。 Unitにはユニットテスト、FeatureにはE2Eテストあるいは機能テスト。を配置するのがお作法です。
実際のテストの作成での細かいことは省きますが1点注意しておくことで、テストクラスに独自のsetUpメソッドを定義する場合、親のクラスのparent::setUp()/parent::tearDown()を確実に呼ぶようにする必要があります。 setUpメソッドを定義するとテストを実行する前に毎回setUp()を実行します。tearDown()も同様です。テストのたびに実行されるのであまり重い処理は書かないほうが良いでしょう。またparent::setUp()/parent::tearDown()の呼ぶ位置も注意が必要でparent::setUp()はsetUp()メソッド内の最初で、parent::tearDown()はtearDown()メソッド内の最後で呼ぶようにします。 setUp()/tearDown()はテスト実行のたびに呼ばれます。クラスで1回に制限したいといった場合には、何某かの処理を記述する必要があります。
class ExampleTest extends TestCase
{
public function setUp() :void
{
parent::setUp();
// 初期化処理
}
public function tearDown() :void
{
// 後処理
parent::tearDown()
}
}
番外編:404エラー
わかってしまえば、そうなのか。という感じだがなんでそうなるかまったく思いつかなかったのでこくはくしてみる。
class ExampleTest extends TestCase
{
public function testBasicTest_01()
{
$response = $this->get('/');
$response->assertStatus(200);
}
public function testBasicTest_02()
{
$response = $this->get('/top');
$response->assertStatus(200);
}
}
とある現象に悩まされてました。それは、2個目の、testBasicTest_02()中のget()呼び出しでstatusコードが404になるといったものでした。 実行順を変えたりしても、必ず2個目のテストで404が返ってくる。テストのコードが原因でないことはわかったので、それ以外を調べ始める。 そして、Route::current()で返り血をチェックしたところ1個目と2個目で明確な差異を見つけた。どうも2個目のテストからrouteが取得できない状態になっているようだ。 そのとき、テストに関連してrouteの設定部分も変更していたことを思い出した。しかしやったことは設定を内容毎に別ファイルに分割するだけで、設定とかはほぼそのままで、動作も特に問題なく動いていた。
Route::get('/', 'RootController@index');
require_once 'web_01.php';
require_once 'web_02.php';
require_once 'web_03.php';
方向性が見えたので、route関係を調べてみる。すると決定的なヒントをphpunitのフォーラムで見つけた。 もったいぶりをやめて答えを書くと、分割したファイルを取り込む処理をrequire_once()からrequire()に変更することで2個目のテストも完走できた。C++脳には2重インクルードの予防くらいの認識だったが、PHPならではの違いを意識する必要があったのだ。詳しくは調べていないが、テストの初期化、オブジェクトの寿命、そしてPHPでのonceがついた場合の挙動。そういったものが関係してそうと予想。 意外なところではまってしまった。普通にwebアプリの場合あまりありそうにないケースだが何かの足しになるかと思い書いてみました。
といったところでお時間になりました。
少しはお役に立てたなら幸いです。
株式会社grasys(グラシス)は、技術が好きで一緒に夢中になれる仲間を募集しています。
grasysは、大規模・高負荷・高集積・高密度なシステムを多く扱っているITインフラの会社です。Google Cloud (GCP)、Amazon Web Services (AWS)、Microsoft Azureの最先端技術を活用してクラウドインフラやデータ分析基盤など、ITシステムの重要な基盤を設計・構築し、改善を続けながら運用しています。
お客様の課題解決をしながら技術を広げたい方、攻めのインフラ技術を習得したい方、とことん技術を追求したい方にとって素晴らしい環境が、grasysにはあります。
お気軽にご連絡ください。