数据库测试

未匹配的标注

数据库测试

简介

Laravel 提供了各种有用的工具,可以更轻松地测试数据库驱动的应用程序。首先,您可以使用 assertDatabaseHas 辅助器来断言数据库中存在符合给定条件的数据。例如,如果您想验证 users 表中是否有一条记录,其email 值为 sally@example.com,则可以执行以下操作:

    public function testDatabase()
    {
        // Make call to application...

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

您也可以使用 assertDatabaseMissing 辅助器来断言数据库中不存在数据。

assertDatabaseHas 方法和其他类似的辅助器是为了方便起见。您可以自由使用 PHPUnit 的任何内置断言方法来补充功能测试。

每次测试后重置数据库

在每次测试后重置数据库通常很有用,这样前一次测试中的数据就不会干扰后续测试。 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模型 定义默认属性集,而无需在创建此测试数据时手动指定每列的值。

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

php artisan make:factory PostFactory

新工厂将放置在您的 database/factories 目录中。

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

php artisan make:factory PostFactory --model=Post

写工厂

首先,请查看应用程序中的 database/factories/UserFactory.php 文件。 开箱即用,此文件包含以下工厂定义:

    namespace Database\Factories;

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

    class UserFactory extends Factory
    {
        /**
         * The name of the factory's corresponding model.
         *
         * @var string
         */
        protected $model = User::class;

        /**
         * Define the model's default state.
         *
         * @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', // password
                'remember_token' => Str::random(10),
            ];
        }
    }

如您所见,工厂是最基本的形式,它们是扩展 Laravel 基本工厂类并定义model 属性和 definition 方法的类。definition 方法返回使用工厂创建模型时应用的默认属性值集。

通过 faker 属性, 工厂可以访问 Faker PHP 函数库, 它允许你便捷的生成各种随机数据来进行测试。

技巧:你也可以在 config/app.php 配置文件中添加 faker_locale 选项来设置 Faker 的语言环境。

工厂状态

工厂状态可以让你任意组合你的模型工厂,仅需要做出适当差异化的修改,就可以达到让模型拥有多种不同的状态。例如, 你的 User 模型中可以修改某个默认属性值来达到标识一种 suspended 状态。你可以使用 state 方法来进行这种状态转换。您可以根据自己的喜好命名状态方法。 毕竟,这只是一个典型的PHP方法:

/**
 * 标识用户 「 已暂停 」 状态。
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
public function suspended()
{
    return $this->state([
        'account_status' => 'suspended',
    ]);
}

如果状态转换需要访问工厂定义的其他属性,则可以将回调传递给 state 方法。 回调将收到为工厂定义的原始属性数组:

/**
 * 标识用户 「 已暂停 」 状态。
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
public function suspended()
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}

工厂回调

工厂回调是使用 afterMakingafterCreating 方法注册的,并且允许你在创建模型之后执行其他任务。 您应该通过在工厂类上定义 configure 方法来注册这些回调。 实例化工厂后,Laravel将自动调用此方法:

namespace Database\Factories;

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

class UserFactory extends Factory
{
    /**
     * 工厂所对应model的名称。
     *
     * @var string
     */
    protected $model = User::class;

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

    // ...
}

使用模型工厂

创建模型

模型工厂定义后,就可以在Eloquent模型上使用 Illuminate \ Database \ Eloquent \ Factories \ HasFactory 特性提供的静态 factory 方法来实例化该模型的工厂实例:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasFactory;
}

让我们看一些创建模型的例子。首先,我们将使用 make 方法创建模型但不将他们保存至数据库中:

use App\Models\User;

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

    // Use model in tests...
}

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

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

HasFactory 特征的 factory 方法将使用默认的约定来确定模型的正确工厂。 具体来说,该方法将在Database \ Factories 命名空间中寻找一个工厂,该工厂的类名与模型名称匹配,并且后缀为Factory 。 如果这些约定不适用于您的特定应用程序或工厂,则可以直接使用工厂来创建模型实例。 要使用factory类创建一个新的工厂实例,应在工厂上调用静态的 new 方法:

use Database\Factories\UserFactory;

$users = UserFactory::new()->count(3)->make();

应用状态

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

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

Overriding Attributes

如果您想覆盖模型的一些默认值,可以将一个值数组传递给 make 方法。只有指定的值将被替换,而其余的值仍设置为工厂指定的默认值:

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

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

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

技巧:批量分配保护 在使用工厂创建模型时,是自动禁用的。

持久化模型

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

    use App\Models\User;

    public function testDatabase()
    {
        // Create a single App\Models\User instance...
        $user = User::factory()->create();

        // Create three App\Models\User instances...
        $users = User::factory()->count(3)->create();

        // Use model in tests...
    }

您可以通过将属性数组传递给 create 方法来覆盖模型上的属性:

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

序列

有时,您可能希望为每个创建的模型替换给定模型属性的值。 您可以通过将状态转换定义为 Sequence 实例来完成此操作。 例如,我们可能希望为每个创建的用户在 User 模型上的 admin 列的值在 YN 之间切换:

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

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

在本例中,将创建 5 个用户,admin 值为 Y,创建另外 5 个用户,admin 值为 N

工厂关系

定义内的关系

您可以将关系附加到工厂定义中的模型。例如,如果您想在创建 Post 时创建一个新的 User 实例,您可以这样做:

    use App\Models\User;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'user_id' => User::factory(),
            'title' => $this->faker->title,
            'content' => $this->faker->paragraph,
        ];
    }

如果关系的列取决于定义它的工厂,您可以提供一个接受求值属性数组的回调:

    /**
     * Define the model's default state.
     *
     * @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,
        ];
    }

多种关系

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

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

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

按照惯例,在将 Post 模型传递给 has 方法时,Laravel 会假设 User 模型必须有一个定义关系的 posts 方法。如有必要,您可以明确指定要操作的关系的名称:

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

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

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

使用魔术方法

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

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

在使用魔术方法创建工厂关系时,您可以传递要在相关模型上覆盖的属性数组:

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

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

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

从属关系

既然我们已经探索了如何使用工厂构建「has many」关系,那么让我们来看看该关系的反面。for 方法可用于定义出厂创建的模型所属的模型。例如,我们可以创建三个属于单个用户的 Post 模型实例:

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

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

使用魔术方法

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

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

多对多关系

像 [has many 关系] 一样,可以使用 has 方法创建「多对多」关系:

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

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

Pivot (中转) 表属性

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

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

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

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

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

使用魔术方法

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

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

多态关系

多态关系 也可以使用工厂创建。多态的 「morph many」关系的创建方式与典型的 「has many」 关系的创建方式相同。例如,如果 Post 模型与 Comment 模型存在 morMany 关系:

    use App\Models\Post;

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

变形关系

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

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

多态多对多关系

可以像创建非多态的 「多对多」关系一样创建多态的「多对多」关系:

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

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

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

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

使用种子

如果您在功能测试时希望使用 数据库种子 填充您的数据库,可以使用 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->assertDatabaseCount($table, int $count); 断言数据表包含给定数量的实体。
$this->assertDatabaseHas($table, array $data); 断言数据表包含给定数据。
$this->assertDatabaseMissing($table, array $data); 断言数据表不包含给定数据。
$this->assertDeleted($table, array $data); 断言给定记录是否被删除。
$this->assertSoftDeleted($table, array $data); 断言给定记录已经被软删除。

为方便起见, 你可以传递一个模型实例到 assertDeletedassertSoftDeleted 函数来断言对应数据库记录是否被删除或软删除,底层依据的是模型主键与数据表记录建立关联。

举个例子, 如果你在你的测试中使用了一个模型工厂,你可以传入这个其中一个模式来帮助测试你的应用是否正确的删除了数据库的记录:

public function testDatabase()
{
    $user = User::factory()->create();

    // 调用这个程序...

    $this->assertDeleted($user);
}

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:6
讨论数量: 0
发起讨论 只看当前版本


暂无话题~