Laravelのファクトリー
以前にLaravelでデータベースの設定をしました。アプリケーションのテストをする際にそのテストデータが必要になる場合が多いと思います。
ここでは、Laravelのテストデータ作成の機能であるファクトリーについて、モデルとの関係性やシーダーからの利用法などの話を含めながら説明します。また、デバッグ時に便利なLaravel Tinkerで状態を確認しながら進めていきます。
Laravelのモデル
MVCにおける「モデル」は「アプリケーションが扱う領域のデータと手続きを表現するもの」と定義されていますが、Laravelで「php artisan make:model ...」として作成される「モデル」は「Eloquent」と呼ばれる機能を持ちます。
「Eloquent」はオブジェクトリレーショナルマッパー(ORM)で、「データベースの行とPHPのオブジェクト(連想配列)を相互変換するもの」です。
つまり、Laravelにおける「モデル」は本来の意味からは外れることはありませんが、それより具体的な機能を持ったものとなります。対応するデータベースのテーブルをもち、その選択や挿入や変更、削除等のUIをなど備えています。このLaravelのモデルを本来の「モデル」と区別して「Eloquentモデル」と呼んでいるようです。
Eloquentモデルの生成時する際は「php artisan make:model モデル名」とします。この時、モデルに結びつけるmigrationやseeder、controller、factoryといったクラスのひな型を同時に生成できます。
PS > php artisan make:model Test -mfsc Model created successfully. Factory created successfully. Created Migration: 2022_07_04_105626_create_tests_table Seeder created successfully. Controller created successfully.
Testモデルを生成したのに対して、マイグレーションではtestsという名前になっているのが確認できます。モデルの命名規則に、モデル名のキャメルケースをスネークケースにして複数形したものがテーブル名になるというものがあります。
この時の複数形は英語で実際に使われる複数形です。例えばモデル名を「HappySheep」とした場合、sheepの複数形はそのままsheepなのでテーブル名は「happy_sheep」となります。わりと使いそうな単語では「RawData」などの「Data」もsがつかず「raw_data」となります。
Tinker
Laravelではphp artisan serveコマンドでデバッグ実行ができたり、php artisan testコマンドも存在しますが、PHPのコードを部分的に確かめたかったりする場合にはそれだと不便なケースもあると思います。
そのような時はLaravel Tinkerを利用します。TinkerはREPL(Read-Eval-Print Loop)と呼ばれる、ユーザーが対話的にPHPのコード片を実行できる環境です。Laravel環境にはデフォルトでインストールされていると思いますが、パッケージ(laravel/tinker)として存在しますので、composer経由で追加(require)や削除(remove)が可能です。
「php artisan tinker」とすると対話モードが始まり「exit」を入力すると終了します。サンプルとして、UserFactory名前空間を宣言して、インスタンスを生成し、definitionメソッドを呼んでみました。
PS > php artisan tinker
Psy Shell v0.11.6 (PHP 8.1.7 — cli) by Justin Hileman
>>> use Database¥Factories¥UserFactory;
>>> $test = Database¥Factories¥UserFactory;
=> Database¥Factories¥UserFactory {#3535}
>>> $test->definition();
=> [
"name" => "Ms. Dawn Russel",
"email" => "lcassin@example.org",
"email_verified_at" => Illuminate¥Support¥Carbon @1657000899 {#3589
date: 2022-07-05 06:01:39.735244 UTC (+00:00),
},
"password" => "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
"remember_token" => "U4dXHXGmye",
]
>>> exit
Tinkerの設定ファイルは、vendor:publishコマンドを使うことでconfig/tinker.phpに出力されます。
ファクトリー
Laravel:「データベーステスト」を参考に進めていきます。
Laravelにはデータベーステストを容易にする機能群を備えています。その機能のひとつにモデルファクトリーがあります。これは、テストデータを生成するものです。
Laravelをインストールすると、databaseフォルダのfactoriesフォルダの中に、UserFactoryというファクトリーがあります(先ほどTinkerの例示に使ったものです)。
これは、Userマイグレーション、Userモデルとセットで存在しています。
UserFactory.php
...
class UserFactory extends Factory{
public function definition(){
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
public function unverified(){
return $this->state(function (array $attributes) {
return [
'email_verified_at' => null,
];
});
}
...
UserFactoryは親クラスのFactoryを継承しておりcreateメソッドを持っています。これを呼ぶと、definitionで設定している値を利用してデータベースへ挿入するのと同時にその行(モデル)を返します。試しにTinkerから利用してみます。
>>> use App¥Models¥User
>>> $user = User::factory()->create();
=> App¥Models¥User {#3600
name: "Dr. Imani Kihn",
email: "dolly.gleason@example.org",
email_verified_at: "2022-07-05 08:40:24",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "immColFtXg",
updated_at: "2022-07-05 08:40:24",
created_at: "2022-07-05 08:40:24",
id: 1,
}
モデルではデフォルトでHasFactoryというトレイトを読み込むようになっていて、上記のコマンド内では、それを利用することで、ファクトリーをモデルから呼び出しています。
UserFactoryで定義されているunverified()メソッドを呼ぶとcreateを実行した際に、email_verified_atにnullを入れるファクトリーが返ります。このファクトリーを返しているのは自身のstateメソッドで、これは変更内容を引数に受け取ます。
...
>>> $userUnverified = User::factory()->unverified();
=> Database¥Factories¥UserFactory {#3580}
>>> $userUnverified->create();
=> App¥Models¥User {#4539
name: "Trisha Abernathy DVM",
email: "rippin.emery@example.net",
email_verified_at: null,
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "Ih8SsU01oi",
updated_at: "2022-07-05 09:01:41",
created_at: "2022-07-05 09:01:41",
id: 102,
}
この挙動は次のようにcreateに連想配列を渡した際と同じですが、definitionの次に多用するようなテストデータなら事前に定義しておくと便利です。
...
>>> User::factory()->create(['email_verified_at'=>null]);
=> App¥Models¥User {#4541
name: "Toni Ullrich",
email: "tremayne08@example.org",
email_verified_at: null,
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "Eou9wcPn9b",
updated_at: "2022-07-05 09:04:40",
created_at: "2022-07-05 09:04:40",
id: 103,
}
Faker
先ほどのUserFactoryの中でfakerというプロパティがありましたが、これにはFakerというテストデータ生成ライブラリが使われています。
Laravel以外でも「composer reuire fakerphp/faker」としてインストールすることが可能です。
インストール後はインスタンスを生成して、生成したいデータのタイプに合わせたメソッドを呼べばデータが返ります。
sample.php
<?php
reuire_once 'vendor/autoload.php';
$faker = Faker¥Factory::create();
echo $faker->name();
echo $faker->email();
echo $faker->text();
上記のコードを実行してみます。またcreateの引数にロケール('ja_JP')を設定すると、日本語の設定にになります。
composer reuire fakerphp/faker ... php sample.php Myles Kulas IV luella.krajcik@example.com Molestiae aut quae eum et rerum. Sint eaque consequuntur illum inventore. Distinctio laudantium animi ... 斉藤 直人 nhamada@example.org Qui illum accusantium dolor. Voluptates error dolores blanditiis provident ipsam saepe. Aliquam rerum voluptatibus voluptatem ea qui. Maxime rerum repudiandae sequi id consequatur.
text()は日本語にならないのかな? と思っていたらrealText()を使うと出せるようです。その結果がつぎのような感じでした。
文章としては成立していませんが、テストデータには十分でしょう。
Laravelではconfig¥app.phpに「faker_locale」というキーがありますので、それをja_JPにすることで日本語設定となります。
ファクトリーとモデルの結びつけ
今度はファクトリーを生成してみます。ファクトリーは「php artisan make:factory ...」として生成できますが、対応するモデルがないとエラーになりますので、ここではモデルと一緒に生成します(make:model -f)。
PS > php artisan make:model HappySheep -f Model created successfully.
モデルの中に「use HasFactory」という記述があり前述の通りデフォルトでトレイトが設定されていることがわかります。
先ほど紹介したTinkerコマンドを使ってモデルに結びついているファクトリーを確認してみます。
PS > php artisan tinker
Psy Shell v0.11.6 (PHP 8.1.7 — cli) by Justin Hileman
>>> use App¥Models¥HappySheep
>>> HappySheep::factory();
=> Database¥Factories¥HappySheepFactory {#3590}
実運用ではモデル命名の規則に従わないファクトリーができるかもしれません。そこで、HappySheepFactoryのファイル名とクラス名をSleepySheepFactoryに変更して、「php artisan cache:clear」コマンドでキャッシュをクリアし、tinkerを再起動して再び同じコマンドを入力してみます。
PHP Error: Class "Database¥Factories¥HappySheepFactory" not found in ...
リネームしたので当然ファクトリーは見つかりません。モデルに特定のファクトリーを設定するには次のようにします。
HappySheep.php
>?php
namespace App¥Models;
use Illuminate¥Database¥Eloquent¥Factories¥HasFactory;
use Illuminate¥Database¥Eloquent¥Model;
use Database¥Factories¥SleepySheepFactory;
class HappySheep extends Model{
use HasFactory;
protected static function newFactory(){
return SleepySheepFactory::new();
}
}
さらに、ファクトリー側からみた際に結びつけるモデルを設定します。
SleepySheepModel.php
<?php
namespace Database¥Factories;
use App¥Models¥HappySheep;
use Illuminate¥Database¥Eloquent¥Factories¥Factory;
class SleepySheepFactory extends Factory{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
protected $model = HappySheep::class;
public function definition(){
return [];
}
}
ファクトリーのコールバック
ファクトリーにはコールバックも設定でき、モデル作成時にコールバックを設定するafterMakingと、モデル作成後のイベントを起こすafterCreatingがあります。
この設定は、ファクトリーのconfigureメソッドで定義します。
ファクトリーでcreateを呼んだ後に古いデータを一つ削除するコードの例は次のようになります。
UserFactory.php
...
public function configure(){
return $this->afterCreating(function(User $user){
$del = User::first();
$del->delete();
});
}...
ファクトリーの他のメソッド
ここまではcreateメソッドを使ってデータベースに登録していましたが、makeメソッドにするとモデル(データ)だけ作成することも可能です。
また、count(10)とすると10件のデータを一度に処理することができます。これはstateにも適用できます。
>>> User::factory()->count(10)->make();
>>> User::factory()->count(15)->unverified()->create();
シーダーとテスト
シーダーはデータベースにひとまとまりのテストデータの挿入をするクラスです。デフォルトで、database¥seedersフォルダに「DatabaseSeeder」が存在する思いますが、これがデータシードのデフォルトのエントリーポイントとなります。
このrunメソッド内にファクトリからのメソッドを書きます。
ここではいままで利用してきたファクトリーを使ってデータを挿入していますが、必ずしもファクトリーを使わないといけないわけではありません。
DatabaseSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder{
public function run() {
¥App¥Models¥User::factory()->count(10)->create();
}
}
ターミナルから「php artisan db:seed」を実行するとこのrunメソッド内に書かれた内容が実行されます。もし、さきほどの1行挿入したら1行消すというファクトリーのイベントを設定した状態でしたら、そのイベントを動かないようにしてください(テーブルにデータが何もない状態から始めるといつまでたっても0件のままです)。
処理が細分化される場合はシーダーを別ファイルに作成することも可能です。
また、シーダーはテストをする際にデータを初期化するようなケースに用いる事が多いと思いますので、テストの方法に関しても少し触れておきます。
このテストファイルのサンプルはデフォルトではtestsフォルダの中にあります。FeatureとUnitフォルダの両方にExampleTest.phpファイルが配置されています。ここではFeatureにあるものを次のように修正しました。
自分で作成する場合は「php artisan make:test TestName」とします。このファイルはFeatureフォルダに入ります。もしUnit側にしたければ--unitオプションを付けます。
ExampleTest.php
<?php
namespace Tests¥Feature;
use Illuminate¥Foundation¥Testing¥RefreshDatabase;
use Tests¥TestCase;
class ExampleTest extends TestCase{
// 最初にデータベースの中身を一旦削除
use RefreshDatabase;
// DatabaseSeederを実行する
protected $seed = true;
public function test_the_application_returns_a_successful_response(){
$response = $this->get('/');
$response->assertStatus(200);
}
}
「use RefreshDatabase;」は書いておくだけで、テスト実行時にデータベースの中身がクリアされます。
ここでは「$seed=true;」とすることで自動でDatabaseSeederを実行していますが、Illuminate¥Foundation¥Testing¥TestCaseを継承したクラスからは$this->seed();としてもDatabaseSeederを実行できます。またこのseedメソッドの引数に任意のシーダーのクラスかその配列を渡すことでシーディングを個別に実行することもできます。
指定したファイルでテストを実行するには「php artisan test tests¥Feature¥ExampleTest」とします。クラス内にあるメソッド毎にテストが実行されます。
PS> php artisan test tests¥Feature¥ExampleTest
またファイルを指定しない場合は、phpunit.xmlに書かれている内容でテストされます。デフォルトの設定ではそれぞれのフォルダでファイル名の終わりがTest.phpとなっているテストファイルを実行するようになっていました。
phpunit.xml
...
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
...