使用工厂状态深入探索工厂
发布于 作者 Paul Redmond
我猜想,如果你熟悉 Laravel,你可能会在应用程序开发中使用 模型工厂,甚至可能使用工厂状态。文档展示了使用工厂进行种子和创建测试数据的机制,但是我想探讨一下有效地使用工厂与模型交互的一些指导思想。
以下是我在更有效地使用工厂状态方面一直在思考的一些方法
首先,创建使用静态值的工厂,而不是为所有内容使用 Faker。
其次,你的工厂应该只创建创建模型实例所需的最小属性集。
使用静态数据代替 Faker
我并不是说使用 Faker 是 _错误的_,而是说静态值可以使测试数据比随机数据更清晰。
考虑以下 `User` 工厂
$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), ];});
代码示例是 Laravel 5.6 附带的用户工厂。工厂定义中没有任何我想批评的内容 - 它完全没问题。但是,我想让你考虑一下,当你运行你的测试套件时,每次都会生成新的值
// First test runarray:5 [ "name" => "Troy Miller III" "email" => "[email protected]" "updated_at" => "2018-04-09 01:35:38" "created_at" => "2018-04-09 01:35:38" "id" => 1] // Second test runarray:5 [ "name" => "Mr. Zachariah McGlynn" "email" => "[email protected]" "updated_at" => "2018-04-09 01:35:41" "created_at" => "2018-04-09 01:35:41" "id" => 1]
你可能认为这种测试数据的随机化是一件 _好事_,它使你的测试套件更健壮。这是一个有效的论点,但考虑一下你需要编写的验证数据的配套测试
public function testWelcomeMessageTest(){ $user = factory('App\User')->create(); $response = $this->get('/'); $response->assertSeeText('Welcome '.$user->name);}
我想再次强调,这个测试本身并没有什么错,但我感觉它涉及了额外的“魔法”,需要你动脑筋才能理解。我使用了一个变量来断言测试通过,而不是使用硬编码的断言,在我看来,这使测试更易读。
考虑以下工厂
$factory->define(App\User::class, function (Faker $faker) { return [ 'name' => 'Example User, 'email' => '[email protected]', // ... ];});
硬编码的名称和电子邮件是一个细微的变化,但现在我的测试可能看起来像这样
public function testWelcomeMessageTest(){ factory('App\User')->create(); $response = $this->get('/'); $response->assertSeeText('Welcome Example User');}
另一种方法是使用具有动态数据的工厂,并覆盖你想要测试的内容
public function testWelcomeMessageTest(){ factory('App\User')->create([ 'name' => 'Example User', ]); $response = $this->get('/'); $response->assertSeeText('Welcome Example User');}
你应该在你的应用程序中做你觉得合适的事情,但我希望你至少考虑一下 _你并不需要为所有内容都使用 Faker_。事实上,通过这种细微的转变,你可能会在测试中获得更清晰的思路。
如果你不想失去 Faker 提供的随机性,可以考虑为你的默认测试使用工厂状态,其中包含一些静态值
$factory->define(App\User::class, 'user', function (Faker $faker) { return [ 'name' => 'Example User, 'email' => '[email protected]', ];});
然后在你的测试中,如果你想要一个基本的静态用户
public function testWelcomeMessageTest(){ factory('App\User')->states('user')->create(); $response = $this->get('/'); $response->assertSeeText('Welcome Example User');}
`user` 状态代表你的模型所需的属性基本集 - 它是创建模型所需的最小属性集。
最小可行属性
创建具有创建模型所需最小数据量的工厂和测试数据是你在测试中应该培养的一个好习惯。如果一个字段是可空的,或者模型具有可选的关系,不要在默认工厂中定义它们。
你的测试将更容易帮助你发现空值和空关系的问题。空模型关系通常是用户在你的应用程序中遇到的第一个状态。
当数据库中的一个列可以为空时,默认情况下在你的工厂中省略它,然后使用状态来进一步测试具有超过最小数据要求的模型。
以论坛应用程序为例。当用户首次注册时,他们还没有创建任何帖子,也没有对其他人的帖子进行任何评论。这代表了你应用程序中最基本的用户,具有最低要求。
拥有工厂数据和 Faker 在你的处置范围内,使你很想尽可能多地填写数据,但只在你默认的模型工厂状态中定义绝对必要的數據。
使用状态增强工厂数据
以论坛应用程序为例,我们可以为具有帖子的用户定义一个状态,在我们的测试需要超过最低要求数据的情况下
$factory ->state(App\User::class, 'with_posts', []) ->afterCreatingState(App\User::class, 'with_posts', function ($user, $faker) { factory(App\Post::class, 5)->create([ 'user_id' => $user->id, ]); });
此状态没有任何覆盖数据,因此我们可以传递一个空数组作为第三个参数,而不是闭包。我们使用 `afterCreatingState` 方法来定义与用户相关联的一些帖子,现在我们可以用它们来进行测试
$user = factory('App\User')->states('with_posts')->create();
这种方法的最大缺点是 `afterCreatingState` 回调中硬编码的 `5` 个帖子。我只是在写伪代码,但类似这样的 API 可能会很好
$user = factory('App\User') ->states('with_posts', ['posts_count' => 5]) ->create();
然后在工厂 API 的另一端,类似这样的内容
$factory ->state(App\User::class, 'with_posts', []) ->afterCreatingState(App\User::class, 'with_posts', function ($user, $faker, $config) { factory(App\Post::class, $config->get('posts_count', 5))->create([ 'user_id' => $user->id, ]); });
再次强调,_这是伪代码,不能正常工作!_但我想要展示几种使这种方法更好的方式。也许这是一个包可以扩展基本工厂功能的领域,以实现这种 API。
即使存在缺点,我们仍然拥有一个很好的声明式方法来使用具有帖子的用户状态。
其他工厂状态方法
我们还可以使用测试框架中的 trait 来为具有帖子的用户创建更动态的状态,以及我们想要添加到用户中的任何其他状态
<?php namespace Tests\factories; trait UserFactory{ public function userWithPosts($config = []) { $user = factory(\App\User::class)->create(); $posts = factory( \App\Post::class, $config['posts_count'] ?? 5 )->make([ 'user_id' => $user->id, ]); $user->posts()->saveMany($posts); return $user; }}
在你的测试中,如果你需要一个具有帖子的用户,你可以使用以下代码
namespace Tests\Feature; use Tests\TestCase;use Tests\factories;use Illuminate\Foundation\Testing\RefreshDatabase; class ExampleTest extends TestCase{ use RefreshDatabase; use factories\UserFactory; public function testWelcomeMessageTest() { $user = $this->userWithPosts(['posts_count' => 5]); // ... }}
我喜欢在顶部定义 `use Tests\factories`,以及 `use factories\UserFactory;` 来导入 trait,因为我觉得这样一眼就能看出 trait 是用于工厂数据的。使用 trait 需要相当多的样板代码,但比具有硬编码为 5 个帖子数量的静态工厂状态更灵活。
了解更多
你可以在 Laravel 文档中了解有关工厂状态的更多信息。如果你想更多地了解工厂的工作原理,另一个很好的来源是一个 Ruby gem Factory Bot,特别是它的 入门文档。
工厂应该是最基本的 是另一个关于使用工厂的优秀资源,由上面提到的 Factory Bot 库的作者编写。