LaravelでTDD

0
359

「Laravel環境構築」の続き(サーバ環境構築からLaravelのインストールまでは「Laravel環境構築」を参照)。
とりあえずLaravelが動くところまでできたので以降は具体的に開発を進めて行くことになりますが、何を作るにせよ真面目にTDD(Test Driven Development)した方が良いと思いますので、そのための環境を整えることにします。
なお、Laravelの開発においてDBは必須ではありませんが、少なくとも個人的にDBが関わらないシステムを扱うことはほぼなく、一方でテストケースの生成時にDBをどのように扱うかということは結構重要なポイントになりますので、ここではDB使用を前提とします。

config/app.php

目的に応じて変更すべき点は色々あるかもしれませんが、最低限「timezone」の設定は変更しておきましょう(ログ採取時など「timezone」が「UTC」のままでは見難いので)。

'timezone' => 'Asia/Tokyo',

config/session.php

「expire_on_close」なるパラメータでブラウザが閉じた場合にセッションを切るかどうか指定できるのですが、デフォルトでは「false」(切らない)になっています。例えばログイン状態のままブラウザを閉じた場合、次にブラウザを起動するとログイン状態が維持されているとするとセキュリティ的に問題だと思いますので、ここは切れるようにしておくべきでしょう。

'expire_on_close' => true,

config/database.php

DBを使用するシステムの開発においては当然ながら開発環境としてDBを用意する必要があります。開発過程の動作確認等で実際にこのDBとデータの入出力を行いますし、特に参照局面を考えるとある程度のデータが登録された状態を維持しておきたくなります。
一方でTDDの実施は自動テスト実行とほぼ同義ですが、自動テストにおいては前述の既存データが邪魔であったり、逆に自動テストの実行結果が普段から使用するDBに反映されると都合が悪かったりということがあります。
よって、自動テストを行う際には通常に使用しているDBとは別にテスト専用DBを用意します(以下、前者を「通常DB」後者を「テストDB」と表記します)。
「config/database.php」ではDBに対するコネクション定義を行いますが、ここで上記DBの使い分けを考慮する必要があります。
具体的にはデフォルト(通常DBアクセス用)として「mysql」と言う定義が存在しますが、それをコピーしてテストDBアクセス用の定義を追加します。

'mysql' => [
    'driver' => 'mysql',
    'url' => env('DATABASE_URL'),
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_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' => true,
    'engine' => null,
    'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
    ]) : [],
],      
'mysql_test' => [
    'driver' => 'mysql',
    'url' => env('DATABASE_URL'),
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'forge').'_test',
    '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' => true,
    'engine' => null,
    'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
    ]) : [],
], 

変更したのは「database」の定義で、「DB_DATABASE」に設定された名称に「_test」を付けるようにしているところだけです。

.env

ここも目的に応じて色々と設定すべきところかと思いますが、とりあえずはDB関連の設定を行っておきます。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<DB名>
DB_USERNAME=<アカウント名>
DB_PASSWORD=<パスワード>

先に「config/database.php」に「mysql」と「mysql_test」の2つの定義を行いましたが、上記で「DB_DATABASE」に「hoge」と設定した場合、「mysql」指定の場合は「hoge」DBに、「mysql_test」指定の場合は「hoge_test」DBに対してアクセスすることになります。
また「DB_CONNECTION」に「mysql」と設定されていることで、特に指定がなければ「mysql」コネクションの方を使用することになります。

テーブル生成

DB自体の生成はphpMyAdmin等を使用して別途行っておいてください。前述したように通常DB(_testなし)とテストDB(_testあり)の両方を作成しておきます。
また、Laravel環境でのテーブル生成にはマイグレーションの仕組みを用いると思いますが、こちらに関しても用意済みであるとします。
この状態で以下のように実行すれば、通常DBに必要なテーブルが生成されます。

php artisan migrate

もし初期値が必要であればシーダを用意して以下のように実行します。

php artisan db:seed --class=<シーダ名>

ここまではあくまで通常DBに対する操作です。
ではテストを考慮した場合はどうなるか?ですが、ここは実はテストケースの作り方に依存します(後述するRefreshDatabaseを使用するかどうかで変わります)。
本記事で推奨する方法においてはテストDBに事前に必要なテーブル構造生成および初期値の設定を行っておく必要がありますので、以下のように実行します。

php artisan migrate --database=mysql_test
php artisan db:seed --class=<シーダ名> --database=mysql_test

要は「–database」オプションで「config/database.php」におけるテストDB用のコネクション定義を使用するように指定すれば良い訳です。
なお、シーダに関しては目的に応じて使い分けや組み合わせての利用が可能であるため、どのような単位でシーダを用意しておくかと言うことはテスト効率に影響するかと思います(ここではその指摘まで)。

phpunit.xml

自動テスト実行に関する定義を行いますが、デフォルト設定があるので、そこに追加変更を加えることになります。
ここも細かく見ていけば色々とあります、とりあえず必須作業としては以下の2点。
1点目は「DB_CONNECTION」の設定を「mysql」から「mysql_test」に変えることです。これを行っておかないと自動テスト時のアクセス先が通常DBになってしまいます。

<server name="DB_CONNECTION" value="mysql_test"/>

2点目はDB_DATABASEの設定の削除です。「config/database.php」の設定によってコネクションに「mysql_test」を選択すればアクセスするDB名は「.env」内「DB_DATABASE」で定義した名称に「_test」を付けたものになるよう調整してあるので、「phpunit.xml」内での「DB_DATABASE」設定は不要(と言うか邪魔)です。よって、該当する以下の記述を削除します。

<server name="DB_DATABASE" value=":memory:"/>

最低限は上記2点で問題ないかと思います。
ただ、もう一点、以下の設定を<php>ブロック内に追加しておくと良いです。

<ini name="memory_limit" value="-1"/>

これは自動テスト(phpunit)実行時に使用できるメモリ量に制約を設けないと言う設定で、上記を指定しておかないと多くのメモリを必要とする動作検証でメモリ不足エラーになったりします。

テストケース作成

最後に実際に動作させるテストケースを作成します。
Unitテストであれば「tests/Unit」配下に、Featureテストであれば「tests/Feature」配下に、ファイル名「<任意の文字列>Test.php」でファイルを作成します(これは「phpunit.xml」内でそのように定義されているからで、変更も可能ですが、特にその必要もないかと思います)。
また、実行対象となる各メソッドは「test<任意の文字列>」という名称を付けるか、アノテーションで「@test」指定する必要があります。メソッド名に用いる文字列は日本語(マルチバイト文字)でも良く、若干長くてもテストの内容が分かる名称を付けておくと良いかと思います。

その他細々したことはネット等で調べていただくとして、特に気をつけておくべき点やお勧めの実装方法に関して触れておきます。

DatabaseTransactionsの使用

DBに関わるテストケースを実装する場合、ネット等ではRefreshDatabaseの使用が推奨されています。これはテスト(クラス単位)の実行ごとに対象DBを「migrate:refresh」する方式で、かつ各テストケース(メソッド)の実行単位でトランザクションを適用し、テストケース終了時にロールバックすると言うものです。要はテストケース実行に際して他のテストケースの影響を極力排除するようにしている訳です。
ただ、テストケースにおいては事前にある程度のデータがDBに登録されていることを期待するものも多く、頻繁に自動テストを繰り返したいTDD的にはこの初期値設定が結構邪魔に思えてきます。
そもそもテストケースごとに終了時にロールバックしているのでテストケース間で影響が残るとも考え難く、あえて「migrate:refresh」する必要があるかが疑問です。

一方で、DatabaseTransactionsはRefreshDatabase以前からある方式で、文字通りテストケースごとにトランザクション化することのみに限定した方式のようです。実際のところRefreshDatabaseとDatabaseTransactionsの差が「migrate:refresh」のみかどうかを確認した訳ではなく、DatabaseTransactionsがありながらRefreshDatabaseが追加されていることを考慮すると判断が難しいところですが、とりあえずは効率を重視してDatabaseTransactionsを採用しておきたいと思います。
これによりテスト実行前に設定しておいたデータをテストケース内で使用できるようになり、一方でテストケースで行ったデータ操作はテスト完了時にロールバックされ次のテストケースには影響を与えない形でテストを実行することができるようになります。

UnitテストにおけるTests\TestCaseの使用

artisanを使用してUnitテストファイルを生成すると親クラスである「TestCase」は「PHPUnit\Framework\TestCase」になっています。
しかし、この状態だとLaravel固有の機能が色々と使えなかったりします(Unitテストはフレームワークに依存しない形で行うべき、というような思想に基づいているとの記述も見受けられましたが、今ひとつ意味不明です)。
個人的にはUnitテストでもLaravelの便利機能は使いたいので「PHPUnit\Framework\TestCase」ではなく「Tests\TestCase」を継承するようにします(Featureテストは最初からこのようになっています)。

テストケース実施前のfaker確保

テストケースにおいてfakerを使用したいケースは多々ありますが、テストケースごとにfakerの取得を行うのは冗長です。
各テストケースに共通で行いたい処理を定義する方法として「setUp」と言うメソッドが用意されていますが、これはテストケースごとに実行されるため、単に各テストケースの最初に実行すべき処理を一元管理できると言うだけで効率的にはなっていません。
「setUpBeforeClass」というメソッドもあり、名称からして共通処理を行う場所として良さそうなのですが、実はdataProviderとの実行順においてはdataProvider実行後に「setUpBeforeClass」が実行されるようで、どうせならdataProviderでの使用も前提としたfakerの確保を行いたいところです。
と言うことで、少々マニアックな方法になりますが、テストクラス内に静的プロパティ(例えば$s_faker)を用意しておき、クラスの外に同プロパティへの設定を行う処理を記述しておくと言う形にすることで期待したような処理が実現可能です。

テストケースのtrait化

前述のfakerの件なども含めてテストの実行を1つのクラスにまとめ、クラスとして共通処理定義などを考えたい場合があります。一方で各テストケースの目的を考えた場合、ある程度グループ分けを行い、各グループごとにファイルを分けて管理したいという欲求もあるかと思います。
この2つを満たす方法として、テストクラスは1つ(または限定数)のみ用意し、各テストケースはtraitとして別ファイル化しておいてテストクラスで読み込むようにします。

ここまでの内容をまとめると以下のような構成になります。
ここではテストクラスとして「Test」を用意し、具体的なテストケースとしてUserモデルの動作確認を行うメソッドを「model_User」traitに実装した場合のUnitテストに関する例を示しています。

tests/Unit/Test.php

<?php  
namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Faker;

// テストケース
use Tests\Unit\model_User;

class Test extends TestCase
{   
    use DatabaseTransactions;
    
    static $s_faker;

    // テストケース
    use model_User;

    // 共通処理等を定義
    ....
}
Test::$s_faker = Faker\Factory::create('ja_JP');

tests/Unit/model_User.php

<?php
namespace Tests\Unit;

use App\Models\User;

trait model_User
{
    /**
     * @group model_User
     */
    public function test_Userモデル_テストケース1()
    {
        ....
    }

    /**
     * @group model_User
     */
    public function test_Userモデル_テストケース2()
    {
        ....
    }

    ....
}

カバレッジ計測

上記までの準備ができればテスト自体は実施可能です。ただ、テストケースの作成および実施において、どの程度の網羅度(カバレッジ)でテストできているかを把握することは重要です。
よって、もう少し頑張ってカバレッジの確認までできるようにしておきます。

Xdebugのインストール

カバレッジの計測にはXdebugが必要になります。このインストール手順は環境ごとに異なっていたり少々面倒だったりしますが、その辺も含めて手厚いサポートがあるので大丈夫です。
まずは以下のURLにアクセスしてください。

https://xdebug.org/wizard

上記画面にはフォームが存在し、「php -i」の結果を入れるように記述されています。
よって、テストを実施する環境(今までの流れに準じて環境構築していればVirtualbox上にVirtualminで構築したサーバ環境になっているかと思いますが)で「php -i」を実行し、その内容(かなりの量が出力されるかと思いますが)をコピーし、上記フォームに入力の上、「Analyse my phpinfo() output」ボタンをクリックします。
上記操作結果、画面上にテスト環境にあったXdebugのインストール方法が表示されると思います。
以下、参考までに私の環境での実行結果を示しておきます。

Summary(抜粋)

PHP Version: 7.2.24
Configuration File Path: /etc/opt/rh/rh-php72
Configuration File: /etc/opt/rh/rh-php72/php.ini
Extensions directory: /opt/rh/rh-php72/root/usr/lib64/php/modules

操作内容(概要)

  • 「xdebug-3.0.0.tgz」をダウンロード(リンクになっているのでURLを取得可能)
  • 「yum groupinstall “Development tools” && yum install php-devel autoconf automake」実行
  • ダウンロードした「xdebug-3.0.0.tgz」の解凍
  • 解凍結果「xdebug-3.0.0」ディレクトリが作成されるので、その中へ移動
  • 「phpize」実行
  • 「./configure」実行
  • 「make」実行
  • 「cp modules/xdebug.so /opt/rh/rh-php72/root/usr/lib64/php/modules」実行
  • 「/etc/opt/rh/rh-php72/php.ini」に「zend_extension = /opt/rh/rh-php72/root/usr/lib64/php/modules/xdebug.so」追記

上記はあくまで参考であり、画面に表示された手順に従って作業してください。

/etc/httpd/conf/httpd.confの書き換え

本来は前述の処理を行うことでカバレッジの計測が可能になるはずですが、Virtualminで作成した環境だと計測結果の確認画面でエラーが出たりします。
先の例でUserモデルをテスト対象とするケースを想定しましたが、このモデルが「User.php」というファイルであった場合、同ファイルに関するカバレッジの出力結果は「TUser.php.html」と言うファイルに格納されます。この名称が問題になります。

Virtualminで仮想ホストの定義を行った際、「/etc/httpd/conf/httpd.conf」にはphpに関するファイルをphp-fpmで処理できるような定義が自動的に追加されるのですが、その内容は以下のような記述になっています。

AddHandler fcgid-script .php

一見問題なさそうな定義ですが、実はAddHandlerでは指定された拡張子「.xxx」がファイル名の途中にあるケースでも対象とみなしてしまいます(拡張子が複数存在することを想定しているらしいですが、正直意味不明です…)。
つまり「hoge.php」「hoge.html.php」までならまだしも「hoge.php.html」もphp関連ファイルとみなしてしまうと言うことで、その結果、該当するファイルの内容表示時に同ファイルを無理やりphp-fpmで処理しようとしてエラーになっている模様です。

上記問題を回避するために、該当箇所を以下のように書き換えます。

<FilesMatch \.php$>
    SetHandler fcgid-script
</FilesMatch>

上記により、末尾の拡張子が「.php」でなければphp-fpmの対象として処理されなくなり、「XXXX.php.html」のような名称のファイルでも正しく表示できるようになります。

テストの実行

やっとテストの実行にたどり着きました。
コマンドの書式としては「<Laravel環境のパス>/vendor/bin/phpunit」に対して色々と引数を与えて実行しますが、毎回あれこれ引数を指定するのも面倒なので.bashrcでalias定義しておきます。

~/.bashrc

export XDEBUG_MODE=coverage

alias test_all='<Laravel環境のパス>/vendor/bin/phpunit --verbose --debug --stop-on-failure'
alias test_group='<Laravel環境のパス>/vendor/bin/phpunit --verbose --debug --stop-on-failure --group'
alias test_coverage_all='<Laravel環境のパス>/vendor/bin/phpunit --coverage-html public/report --verbose --debug --stop-on-failure'
alias test_coverage_group='<Laravel環境のパス>/vendor/bin/phpunit --coverage-html public/report --verbose --debug --stop-on-failure --group'

Xdebugのver.3からは「XDEBUG_MODE」の設定が必要らしく、それも.bashrc内で行っておきます。
各aliasは以下のような用途です。

test_all:実行可能な全テストケース実施(カバレッジ計測なし)
test_group:指定されたグループのテストケース実施(カバレッジ計測なし)
test_coverage_all:実行可能な全テストケース実施(カバレッジ計測あり)
test_coverage_group:指定されたグループのテストケース実施(カバレッジ計測あり)

グループに関しては各テストケースのアノテーションで「@group xxxx」のように記述することで細かなグループ分けができます。1つのテストケースに複数のグループ名を割り当てることも可能です。よって、このグループを適切に定義することでテスト効率がかなりアップします。
グループ指定が可能なaliasは文末が「–group」で終わっていますが、このオプションは直後にグループ名が指定されることを期待するものですので、実際には

test_group <グループ名>

のような書式で使うことになります。

「–verbose」「–debug」オプションをつけることで現在実行中のテストケースに関する詳細情報が画面出力されるようになります(指定しないと単に「.」マークが点々と表示されるだけです)。
「–stop-on-failure」オプションをつけることでエラーが発生した場合そこでテスト実行が終了します。エラーの発生に関わらず一通りのテストを実行したい場合はつけないように変更してください。
「–coverage-html」オプションをつけることでカバレッジの計測を行います。厳密には本オプションは計測結果をHTMLとしてブラウザから参照可能な形で出力すると言うもので、これらファイル群の出力先を指定する必要があります。上記設定例では出力先が「public/report」になっていますが、publicがLaravel環境におけるドキュメントルートなので、上記計測結果は以下のようなURLで参照できることになります。

https://<ドメイン or IPアドレス>/report/

これくらいの環境準備ができると、やっとTDDっぽくなります。
開発を効率良く進めるための施策は他にもたくさんあるかと思いますが、今回はここまで。