Skip to content

数据库测试

介绍

Laravel 提供了多种有用的工具和断言,使得测试数据库驱动的应用程序变得更加容易。此外,Laravel 的模型工厂和填充器使得使用应用程序的 Eloquent 模型和关系创建测试数据库记录变得轻而易举。我们将在接下来的文档中讨论所有这些强大的功能。

每次测试后重置数据库

在进一步讨论之前,让我们讨论如何在每次测试后重置数据库,以便前一个测试的数据不会干扰后续测试。Laravel 提供的 Illuminate\Foundation\Testing\RefreshDatabase trait 可以为你处理这个问题。只需在你的测试类中使用该 trait:

php
<?php

namespace Tests\Feature;

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

class ExampleTest extends TestCase
{
    use RefreshDatabase;

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

        // ...
    }
}

定义模型工厂

概念概述

首先,让我们谈谈 Eloquent 模型工厂。在测试时,你可能需要在执行测试之前向数据库插入一些记录。与其在创建测试数据时手动指定每个列的值,Laravel 允许你为每个 Eloquent 模型 定义一组默认属性,使用模型工厂。

要查看如何编写工厂的示例,请查看应用程序中的 database/factories/UserFactory.php 文件。此工厂包含在所有新的 Laravel 应用程序中,并包含以下工厂定义:

php
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * 定义模型的默认状态。
     *
     * @return array
     */
    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', // 密码
            'remember_token' => Str::random(10),
        ];
    }
}

如你所见,在其最基本的形式中,工厂是扩展 Laravel 基础工厂类并定义 definition 方法的类。definition 方法返回创建模型时应应用的默认属性值集。

通过 faker 属性,工厂可以访问 Faker PHP 库,这使得你可以方便地生成各种类型的随机数据进行测试。

lightbulb

你可以通过在 config/app.php 配置文件中添加 faker_locale 选项来设置应用程序的 Faker 语言环境。

生成工厂

要创建工厂,请执行 make:factory Artisan 命令

php
php artisan make:factory PostFactory

新的工厂类将被放置在你的 database/factories 目录中。

模型和工厂发现约定

定义工厂后,你可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 提供给模型的静态 factory 方法来实例化该模型的工厂实例。

HasFactory trait 的 factory 方法将使用约定来确定模型的适当工厂。具体来说,该方法将在 Database\Factories 命名空间中查找与模型名称匹配并以 Factory 结尾的类。如果这些约定不适用于你的特定应用程序或工厂,你可以覆盖模型上的 newFactory 方法以直接返回模型对应工厂的实例:

php
use Database\Factories\Administration\FlightFactory;

/**
 * 为模型创建一个新的工厂实例。
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
protected static function newFactory()
{
    return FlightFactory::new();
}

接下来,在相应的工厂上定义一个 model 属性:

php
use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Factory;

class FlightFactory extends Factory
{
    /**
     * 工厂对应模型的名称。
     *
     * @var string
     */
    protected $model = Flight::class;
}

工厂状态

状态操作方法允许你定义可以以任意组合应用于模型工厂的离散修改。例如,你的 Database\Factories\UserFactory 工厂可能包含一个 suspended 状态方法,该方法修改其默认属性值之一。

状态转换方法通常调用 Laravel 基础工厂类提供的 state 方法。state 方法接受一个闭包,该闭包将接收为工厂定义的原始属性数组,并应返回要修改的属性数组:

php
/**
 * 表示用户被暂停。
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
public function suspended()
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}

工厂回调

工厂回调使用 afterMakingafterCreating 方法注册,允许你在创建或制作模型后执行其他任务。你应该通过在工厂类上定义一个 configure 方法来注册这些回调。Laravel 在实例化工厂时会自动调用此方法:

php
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * 配置模型工厂。
     *
     * @return $this
     */
    public function configure()
    {
        return $this->afterMaking(function (User $user) {
            //
        })->afterCreating(function (User $user) {
            //
        });
    }

    // ...
}

使用工厂创建模型

实例化模型

定义工厂后,你可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 提供给模型的静态 factory 方法来实例化该模型的工厂实例。让我们看几个创建模型的示例。首先,我们将使用 make 方法创建模型而不将其持久化到数据库:

php
use App\Models\User;

public function test_models_can_be_instantiated()
{
    $user = User::factory()->make();

    // 在测试中使用模型...
}

你可以使用 count 方法创建多个模型的集合:

php
$users = User::factory()->count(3)->make();

应用状态

你还可以将任何 状态 应用于模型。如果你想对模型应用多个状态转换,你可以直接调用状态转换方法:

php
$users = User::factory()->count(5)->suspended()->make();

覆盖属性

如果你想覆盖模型的一些默认值,可以将值数组传递给 make 方法。只有指定的属性会被替换,而其余属性将保持为工厂指定的默认值:

php
$user = User::factory()->make([
    'name' => 'Abigail Otwell',
]);

或者,可以直接在工厂实例上调用 state 方法以执行内联状态转换:

php
$user = User::factory()->state([
    'name' => 'Abigail Otwell',
])->make();
lightbulb

使用工厂创建模型时,批量赋值保护 会自动禁用。

持久化模型

create 方法使用 Eloquent 的 save 方法实例化模型实例并将其持久化到数据库:

php
use App\Models\User;

public function test_models_can_be_persisted()
{
    // 创建一个 App\Models\User 实例...
    $user = User::factory()->create();

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

    // 在测试中使用模型...
}

你可以通过将属性数组传递给 create 方法来覆盖工厂的默认模型属性:

php
$user = User::factory()->create([
    'name' => 'Abigail',
]);

序列

有时你可能希望为每个创建的模型交替更改给定模型属性的值。你可以通过将状态转换定义为序列来实现这一点。例如,你可能希望在每个创建的用户之间交替更改 admin 列的值为 YN

php
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
                ->count(10)
                ->state(new Sequence(
                    ['admin' => 'Y'],
                    ['admin' => 'N'],
                ))
                ->create();

在此示例中,将创建五个 admin 值为 Y 的用户,并创建五个 admin 值为 N 的用户。

如果需要,你可以将闭包作为序列值包含。每次序列需要新值时,都会调用闭包:

php
$users = User::factory()
                ->count(10)
                ->state(new Sequence(
                    fn ($sequence) => ['role' => UserRoles::all()->random()],
                ))
                ->create();

在序列闭包中,你可以访问注入到闭包中的序列实例的 $index$count 属性。$index 属性包含到目前为止序列的迭代次数,而 $count 属性包含序列将被调用的总次数:

php
$users = User::factory()
                ->count(10)
                ->sequence(fn ($sequence) => ['name' => 'Name '.$sequence->index])
                ->create();

工厂关系

一对多关系

接下来,让我们探索使用 Laravel 的流畅工厂方法构建 Eloquent 模型关系。首先,假设我们的应用程序有一个 App\Models\User 模型和一个 App\Models\Post 模型。此外,假设 User 模型定义了与 PosthasMany 关系。我们可以使用 Laravel 工厂提供的 has 方法创建一个拥有三个帖子(posts)的用户。has 方法接受一个工厂实例:

php
use App\Models\Post;
use App\Models\User;

$user = User::factory()
            ->has(Post::factory()->count(3))
            ->create();

按照惯例,当将 Post 模型传递给 has 方法时,Laravel 将假定 User 模型必须有一个定义关系的 posts 方法。如果需要,你可以显式指定要操作的关系名称:

php
$user = User::factory()
            ->has(Post::factory()->count(3), 'posts')
            ->create();

当然,你可以对相关模型执行状态操作。此外,如果你的状态更改需要访问父模型,你可以传递基于闭包的状态转换:

php
$user = User::factory()
            ->has(
                Post::factory()
                        ->count(3)
                        ->state(function (array $attributes, User $user) {
                            return ['user_type' => $user->type];
                        })
            )
            ->create();

使用魔术方法

为了方便起见,你可以使用 Laravel 的魔术工厂关系方法来构建关系。例如,以下示例将使用约定来确定相关模型应通过 User 模型上的 posts 关系方法创建:

php
$user = User::factory()
            ->hasPosts(3)
            ->create();

使用魔术方法创建工厂关系时,你可以传递一个属性数组以覆盖相关模型:

php
$user = User::factory()
            ->hasPosts(3, [
                'published' => false,
            ])
            ->create();

如果你的状态更改需要访问父模型,你可以提供基于闭包的状态转换:

php
$user = User::factory()
            ->hasPosts(3, function (array $attributes, User $user) {
                return ['user_type' => $user->type];
            })
            ->create();

属于关系

现在我们已经探索了如何使用工厂构建“一对多”关系,让我们探索关系的反向。for 方法可用于定义工厂创建的模型所属的父模型。例如,我们可以创建三个属于单个用户的 App\Models\Post 模型实例:

php
use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
            ->count(3)
            ->for(User::factory()->state([
                'name' => 'Jessica Archer',
            ]))
            ->create();

如果你已经有一个应该与创建的模型关联的父模型实例,你可以将模型实例传递给 for 方法:

php
$user = User::factory()->create();

$posts = Post::factory()
            ->count(3)
            ->for($user)
            ->create();

使用魔术方法

为了方便起见,你可以使用 Laravel 的魔术工厂关系方法来定义“属于”关系。例如,以下示例将使用约定来确定这三个帖子应属于 Post 模型上的 user 关系:

php
$posts = Post::factory()
            ->count(3)
            ->forUser([
                'name' => 'Jessica Archer',
            ])
            ->create();

多对多关系

一对多关系 类似,“多对多”关系可以使用 has 方法创建:

php
use App\Models\Role;
use App\Models\User;

$user = User::factory()
            ->has(Role::factory()->count(3))
            ->create();

中间表属性

如果你需要定义应在链接模型的中间表上设置的属性,可以使用 hasAttached 方法。此方法接受一个中间表属性名称和值的数组作为其第二个参数:

php
use App\Models\Role;
use App\Models\User;

$user = User::factory()
            ->hasAttached(
                Role::factory()->count(3),
                ['active' => true]
            )
            ->create();

如果你的状态更改需要访问相关模型,你可以提供基于闭包的状态转换:

php
$user = User::factory()
            ->hasAttached(
                Role::factory()
                    ->count(3)
                    ->state(function (array $attributes, User $user) {
                        return ['name' => $user->name.' Role'];
                    }),
                ['active' => true]
            )
            ->create();

如果你已经有想要附加到创建的模型的模型实例,可以将模型实例传递给 hasAttached 方法。在此示例中,相同的三个角色将附加到所有三个用户:

php
$roles = Role::factory()->count(3)->create();

$user = User::factory()
            ->count(3)
            ->hasAttached($roles, ['active' => true])
            ->create();

使用魔术方法

为了方便起见,你可以使用 Laravel 的魔术工厂关系方法来定义多对多关系。例如,以下示例将使用约定来确定相关模型应通过 User 模型上的 roles 关系方法创建:

php
$user = User::factory()
            ->hasRoles(1, [
                'name' => 'Editor'
            ])
            ->create();

多态关系

多态关系 也可以使用工厂创建。多态的“多态多”关系与典型的“一对多”关系的创建方式相同。例如,如果 App\Models\Post 模型与 App\Models\Comment 模型有一个 morphMany 关系:

php
use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();

多态关系

不能使用魔术方法创建 morphTo 关系。相反,必须直接使用 for 方法,并且必须显式提供关系的名称。例如,假设 Comment 模型有一个定义 morphTo 关系的 commentable 方法。在这种情况下,我们可以使用 for 方法直接创建三个属于单个帖子的评论:

php
$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();

多态多对多关系

多态的“多对多” (morphToMany / morphedByMany) 关系可以像非多态的“多对多”关系一样创建:

php
use App\Models\Tag;
use App\Models\Video;

$videos = Video::factory()
            ->hasAttached(
                Tag::factory()->count(3),
                ['public' => true]
            )
            ->create();

当然,魔术 has 方法也可以用于创建多态的“多对多”关系:

php
$videos = Video::factory()
            ->hasTags(3, ['public' => true])
            ->create();

在工厂中定义关系

要在模型工厂中定义关系,通常会将新的工厂实例分配给关系的外键。这通常用于“反向”关系,例如 belongsTomorphTo 关系。例如,如果你想在创建帖子时创建一个新用户,可以执行以下操作:

php
use App\Models\User;

/**
 * 定义模型的默认状态。
 *
 * @return array
 */
public function definition()
{
    return [
        'user_id' => User::factory(),
        'title' => $this->faker->title(),
        'content' => $this->faker->paragraph(),
    ];
}

如果关系的列依赖于定义它的工厂,你可以将闭包分配给属性。闭包将接收工厂的评估属性数组:

php
/**
 * 定义模型的默认状态。
 *
 * @return array
 */
public function definition()
{
    return [
        'user_id' => User::factory(),
        'user_type' => function (array $attributes) {
            return User::find($attributes['user_id'])->type;
        },
        'title' => $this->faker->title(),
        'content' => $this->faker->paragraph(),
    ];
}

运行填充器

如果你想在功能测试期间使用 数据库填充器 来填充数据库,可以调用 seed 方法。默认情况下,seed 方法将执行 DatabaseSeeder,它应该执行所有其他填充器。或者,你可以将特定的填充器类名传递给 seed 方法:

php
<?php

namespace Tests\Feature;

use Database\Seeders\OrderStatusSeeder;
use Database\Seeders\TransactionStatusSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * 测试创建新订单。
     *
     * @return void
     */
    public function test_orders_can_be_created()
    {
        // 运行 DatabaseSeeder...
        $this->seed();

        // 运行特定的填充器...
        $this->seed(OrderStatusSeeder::class);

        // ...

        // 运行特定填充器的数组...
        $this->seed([
            OrderStatusSeeder::class,
            TransactionStatusSeeder::class,
            // ...
        ]);
    }
}

或者,你可以指示 Laravel 在每个使用 RefreshDatabase trait 的测试之前自动填充数据库。你可以通过在基测试类上定义一个 $seed 属性来实现这一点:

php
<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    /**
     * 指示是否应在每个测试之前运行默认填充器。
     *
     * @var bool
     */
    protected $seed = true;
}

$seed 属性为 true 时,测试将在每个使用 RefreshDatabase trait 的测试之前运行 Database\Seeders\DatabaseSeeder 类。但是,你可以通过在测试类上定义一个 $seeder 属性来指定应执行的特定填充器:

php
use Database\Seeders\OrderStatusSeeder;

/**
 * 在每个测试之前运行特定的填充器。
 *
 * @var string
 */
protected $seeder = OrderStatusSeeder::class;

可用断言

Laravel 为你的 PHPUnit 功能测试提供了几个数据库断言。我们将在下面讨论每个断言。

assertDatabaseCount

断言数据库中的表包含给定数量的记录:

php
$this->assertDatabaseCount('users', 5);

assertDatabaseHas

断言数据库中的表包含与给定键/值查询约束匹配的记录:

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

assertDatabaseMissing

断言数据库中的表不包含与给定键/值查询约束匹配的记录:

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

assertDeleted

assertDeleted 断言给定的 Eloquent 模型已从数据库中删除:

php
use App\Models\User;

$user = User::find(1);

$user->delete();

$this->assertDeleted($user);

assertSoftDeleted 方法可用于断言给定的 Eloquent 模型已被“软删除”:

php
$this->assertSoftDeleted($user);

assertModelExists

断言给定的模型存在于数据库中:

php
use App\Models\User;

$user = User::factory()->create();

$this->assertModelExists($user);

assertModelMissing

断言给定的模型不存在于数据库中:

php
use App\Models\User;

$user = User::factory()->create();

$user->delete();

$this->assertModelMissing($user);