Laravel 数据库测试
简介
Laravel 提供了多个有用的工具让测试数据库驱动的应用变得更加简单。首先,你可以使用辅助函数 assertDatabaseHas 来断言数据库中的数据是否和给定数据集合匹配。例如,如果你想要通过 email 值为 sally@example.com
的条件去数据表 users 查询是否存在该记录 ,我们可以这样做:
public function testDatabase()
{
// Make call to application...
$this->assertDatabaseHas('users', [
'email' => 'admin@163.com'
]);
}
我们还可以使用 assertDatabaseMissing 辅助函数断言数据在数据库中不存在。
当然,assertDatabaseHas 方法和其它类似辅助方法都是为了方便起见进行的封装,你也可以使用其它 PHPUnit 内置的断言方法来进行测试。
生成模型工厂
模型工厂可用于快速填充数据表。要创建一个模型工厂,可以使用 Artisan 命令 make:factory
:
$ php artisan make:factory PostFactory
新创建的工厂类位于 database/factories 目录下。
--model
选项可用于指示模型工厂对应的模型类。该选项通过给定的模型名称预填充生成的工厂类:
$ php artisan make:factory PostFactory --model=Post
生成的 PostFactory 内容如下:
<?php
use Faker\Generator as Faker;
$factory->define(App\Post::class, function (Faker $faker) {
return [
//
];
});
每次测试后重置数据库
每次测试后重置数据库通常很有用,这样的话上次测试的数据不会影响下一次测试。RefreshDatabase Trait 基于你是用的是内存数据库还是关系数据库使用最优方式来迁移测试数据库。在测试类上使用这个 trait,一切都不需要操心,系统会自动帮你在每次测试后重置数据库:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
$response = $this->get('/');
// ...
}
}
编写工厂
测试时,通常需要在执行测试前插入新数据到数据库。在创建测试数据时,Laravel 允许我们使用模型工厂为每个 Eloquent 模型定义默认的属性值集合,而不用手动为每一列指定值。作为开始,我们看一下 database/factories/UserFactory.php 文件,该文件包含了一个工厂定义:
use Faker\Generator as Faker;
use Illuminate\Support\Str;
$factory->define(App\User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
});
在闭包中,作为工厂定义,我们返回该模型上所有属性默认测试值。该闭包接收 PHP 库 Faker
实例,从而允许你方便地为测试生成多种类型的随机数据。
我们还可以为每个模型创建额外的工厂文件以便更好地组织管理,例如,你可以在 database/factories
目录下创建 UserFactory.php
和 CommentFactory.php
文件。 factories 目录下的所有文件都会被Laravel 自动加载。
注:可以通过添加
faker_locale
配置项到 config/app.php 配置文件来设置 Faker 的本地化。
继承工厂
如果我们继承了一个模型,可能希望同时继承对应模型的工厂以便在测试和数据填充期间只设置子模型的属性。要实现这个功能,可以调用工厂构建器的 raw 方法从给定工厂中获取原生属性数组:
$factory->define(App\Admin::class, function (Faker\Generator $faker) {
return factory(App\User::class)->raw([
// ...
]);
});
工厂状态
状态允许我们在任意组合中定义可用于模型工厂的离散修改,例如, User 模型可能有一个 delinquent 状态用于修改某个默认属性值,你可以使用 state 方法来定义状态转化。对于简单的状态,可以传递一个属性修改数组:
$factory->state(App\User::class, 'delinquent', [
'account_status' => 'delinquent',
]);
如果我们的状态需要计算或一个 $faker
实例,可以使用一个闭包来计算状态的属性修改:
$factory->state(App\User::class, 'address', function ($faker) {
return [
'address' => $faker->address,
];
});
工厂回调
工厂回调通过 afterMaking
和 afterCreating
方法注册,允许在创建工厂后执行额外的任务,例如,我们可以使用回调来关联额外的模型到已创建的模型:
$factory->afterMaking(App\User::class, function ($user, $faker) {
// ...
});
$factory->afterCreating(App\User::class, function ($user, $faker) {
$user->accounts()->save(factory(App\Account::class)->make());
});
我们还可以为工厂状态定义回调:
$factory->afterMakingState(App\User::class, 'delinquent', function ($user, $faker) {
// ...
});
$factory->afterCreatingState(App\User::class, 'delinquent', function ($user, $faker) {
// ...
});
使用工厂
创建模型
定义好工厂后,可以在测试或数据库填充文件中通过全局的 factory 方法使用它们来生成模型实例,所以,让我们看一些创建模型的例子,首先,我们使用 make 方法,该方法创建模型但不将其保存到数据库:
public function testDatabase(){
$user = factory(App\User::class)->make();
// Use model in tests...
}
还可以创建多个模型集合或者创建给定类型的模型:
// Create three App\User instances...
$users = factory(App\User::class, 3)->make();
应用状态
还可以应用任意状态到模型,如果想要应用多个状态转化到模型,需要指定每个我们想要应用的状态名:
$users = factory(App\User::class, 5)->states('deliquent')->make();
$users = factory(App\User::class, 5)->states('premium', 'deliquent')->make();
覆盖属性
如果想要覆盖模型中的某些默认值,可以传递数组值到 make 方法,只有指定值才会被替换,剩下值保持工厂指定的默认值不变:
$user = factory(App\User::class)->make([
'name' => 'Abigail',
]);
注:使用模型工厂创建模型时批量赋值保护默认会自动禁用。
持久化模型
create
方法不仅能创建模型实例,还可以使用 Eloquent 的 save
方法将它们保存到数据库:
public function testDatabase()
{
// Create a single App\User instance...
$user = factory(App\User::class)->create();
// Create three App\User instances...
$users = factory(App\User::class, 3)->create();
// Use model in tests...
}
我们可以通过传递数组到 create
方法覆盖模型上的属性:
$user = factory(App\User::class)->create([
'name' => 'Abigail',
]);
关联关系
在本例中,我们添加一个关联到创建的模型,使用 create
方法创建多个模型的时候,会返回一个 Eloquent 集合实例,从而允许使用集合提供的所有方法,例如 each
:
$users = factory(App\User::class, 3)
->create()
->each(function($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
可以使用 createMany
方法创建多个关联模型:
$user->posts()->createMany(
factory(App\Post::class, 3)->make()->toArray()
);
关联关系 & 属性闭包
还可以使用工厂中的闭包属性添加关联关系到模型,例如,如果想要在创建 Post
的时候创建一个新的 User
实例,可以这么做:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => factory(App\User::class),
];
});
这些闭包还接收包含它们的工厂属性数组:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => factory(App\User::class),
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
},
];
});
使用 seeds
如果想要在测试期间使用数据库填充器初始化数据库,可以使用 seed
方法。默认情况下,seed
方法会返回 DatabaseSeeder
以便可以执行所有其他填充器,当然,也可以传递指定的填充器类名到 seed
方法:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use OrderStatusSeeder;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* Test creating a new order.
*
* @return void
*/
public function testCreatingANewOrder()
{
// Run the DatabaseSeeder...
$this->seed();
// Run a single seeder...
$this->seed(OrderStatusSeeder::class);
// ...
}
}
有效的断言方法
Laravel 为 PHPUnit
测试提供了多个数据库断言方法:
方法 | 描述 |
---|---|
$this->assertDatabaseHas($table, array $data); | 断言数据表包含给定数据 |
$this->assertDatabaseMissing($table, array $data); | 断言数据表不包含给定数据 |
$this->assertDeleted($table, array $data); | 断言给定记录是否被删除 |
$this->assertSoftDeleted($table, array $data); | 断言给定记录已经被软删除 |
为了方便起见,我们可以传递一个模型实例到 assertDeleted
和 assertSoftDeleted
函数来断言对应数据库记录是否被删除或软删除,底层依据的是模型主键与数据表记录建立关联。
例如,如果我们在测试中使用了模型工厂,可以传递这个模型到其中一个函数来测试应用是否删除了对应的数据库记录:
public function testDatabase()
{
$user = factory(App\User::class)->create();
// Make call to application...
$this->assertDeleted($user);
}