数据库测试

简介

Laravel 为测试使用数据库的应用提供了各种有用的工具。首先,可以使用 assertDatabaseHas 辅助方法断言数据库中是否存在与给定的数组数据相匹配的数据。例如,如果要验证 users 数据表中是否存在 email 值为 sally@example.com 的记录,可以这样做:

public function testDatabase()
{
    // 调用应用

    $this->assertDatabaseHas('users', [
        'email' => 'sally@example.com'
    ]);
}

也可以使用 assertDatabaseMissing 辅助方法断言数据在数据库中不存在。

当然,assertDatabaseHas 方法和其它类似的辅助方法都是为了方便。您可以自由使用任何 PHPUnit 自带的断言方法补充测试。

生成工厂

要创建工厂,可以使用 Artisan 命令 make:factory

php artisan make:factory PostFactory

新的工厂会放在 database/factories 目录中。

--model 选项可用于指定工厂所创建的模型的名称。此选项会使用给定模型预先填充生成的工厂文件:

php artisan make:factory PostFactory --model=Post

每次测试后重置数据库

在每次测试后重置数据库通常很有用,这样之前的测试就不会影响后续的测试。RefreshDatabase Trait 会根据使用的是内存数据库该是传统的数据库,使用最佳方法迁移测试数据库。在测试类中使用此 Trait,一切都将为您处理:

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * 一个基本的功能测试示例
     *
     * @return void
     */
    public function testBasicExample()
    {
        $response = $this->get('/');

        // ...
    }
}

编写工厂

测试时,可能还需要在执行测试前在数据库中插入一些记录。Laravel 允许您为每个使用模型工厂的 Eloquent 模型 定义一组默认的属性,而不是在创建这些测试数据时手动为每个字段指定值。首先,我们下一下应用的 database/factories/UserFactory.php 文件。此文件包含一个开箱即用的工厂定义:

use Faker\Generator as Faker;

$factory->define(App\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
        'remember_token' => str_random(10),
    ];
});

在定义工厂的闭包中,可以返回模型上所有属性的默认测试值。闭包接收一个 Faker PHP 库的实例,您可以方便地生成各种类型的随机测试数据。

为了更好地组织代码,还可以为每个模型都创建一个工厂文件。例如,可以在 database/factories 目录中创建 UserFactory.phpCommentFactory.php 文件。Laravel 会自动加载 factories 目录中的所有文件。

可以通过在 config/app.php 配置文件中添加一个 faker_locale 选项设置 Faker 的首选语言。

工厂状态

状态允许您定义可以任意组合并应用到模型工厂的单独更改。例如,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,
    ];
});

工厂回调

工厂回调使用 afterMakingafterCreating 方法注册,允许您在生成或创建模型后执行其它任务。例如,可以使用回调将其它模型关联到创建的模型:

$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();

    // 在测试中使用模型
}

也可以创建包含很多模型的集合或者创建给定类型的模型:

// Create three App\User instances...
$users = factory(App\User::class, 3)->make();

应用状态

也可以将任何 状态 应用到模型。如果想要应用多个状态修改到模型,可以指定要应用的每个状态的名称:

$users = factory(App\User::class, 5)->states('delinquent')->make();

$users = factory(App\User::class, 5)->states('premium', 'delinquent')->make();

重写属性

如果想要重写模型的某些默认值,可以传递一个值的数组给 make 方法。只有指定的值会被替换而剩下的值仍会设置为工程中指定的默认值:

$user = factory(App\User::class)->make([
    'name' => 'Abigail',
]);

持久化模型

create 方法不仅会创建模型实例,还会使用 Eloquent 的 save 方法将其保存到数据库中:

public function testDatabase()
{
    // 创建单个 App\User 实例
    $user = factory(App\User::class)->create();

    // 创建三个 App\User 实例
    $users = factory(App\User::class, 3)->create();

    // 在测试中使用模型
}

可以通过传递一个数组给 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());
            });

关联 & 属性闭包

也可以在定义工厂时使用闭包属性为模型添加关联。例如,如果要在创建 Post 时创建一个新的 User 实例,可以这样做:

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});

这些闭包也接收定义它们的工厂的属性数组:

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        },
        'user_type' => function (array $post) {
            return App\User::find($post['user_id'])->type;
        }
    ];
});

可用的断言

Laravel 为 PHPUnit 测试提供几个数据库断言:

方法 描述
$this->assertDatabaseHas($table, array $data); 断言数据库的数据表中包含给定数据
$this->assertDatabaseMissing($table, array $data); 断言数据库的数据表中不包含给定数据
$this->assertSoftDeleted($table, array $data); 断言给定记录已被软删除