Laravel factory 使用指引

如果你想为你的 Laravel 项目写一些测试,那么你可能需要在某个时候编写一些工厂模式。 当我第一次听到工厂一词时,我不知道它的含义和作用,更不用说了解它们可以为你的测试带来的好处了。

假设你有一个产品 Controller,该控制器具有一种存储方法来保存新产品的详细信息。 产品可能具有产品代码,标题,价格,描述和标签等属性,这些都在请求中发送到 store 方法。

 

如果你想测试这个 endpoint,可以创建一个属性数组,然后在 POST 请求中发送它

$product = [
    'product_code' => 'ABC123',  
    'title' => 'My Amazing Product', 
    'price' => 100, 
    'description' => 'This product will change the way you wash your dishes forever',
    'tagline' => 'Voted best in category'
];

$response = $this->post(route('products.store'), $product);

// 你的断言
$response->assertSuccessful();

这么做没问题。

 

但是如果你想在另一个测试中使用该 product,比如测试更新 product,你不得不在下一个测试方法中复制该数组, 或者可以将其提取到测试的 setUp () 方法中 并使其成为 $this->product 以重复利用。

 

如果你还有另一个测试类要测试将 product 添加到 category 中,那你该怎么办?怎样才能重用你的产品代码?你会如何定义不同模型之间的关系? 幸运的是,工厂模式可以解决这些问题。

Creating a factory

你可以通过创建相应的工厂方法来产生你所需要的数据。 这里提供了一个 artisan 命令可以帮助你快速的、根据你的模型创建对应的工厂方法。

php artisan make:factory ProductFactory --model=Product

执行上面命令后,你会在 database/factories 目录下看到一个以你的 Product 模型为基础的,名为 ProductFactory.php 的文件,你可以通过自定义字段名,以及该字段需要的值来获取你需要的数据。下面是一个例子:

use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(App\Product::class, function (Faker $faker) {
    return [
        'product_code' => 'ABC123',
        'title' => 'My Amazing Product', 
        'price' => 100, 
        'description' => 'This product will change the way you wash your dishes forever',
        'tagline' => 'Voted best in category'
    ];
});

使用 Faker 定义假数据

我们可以使用先前定义的数组中的静态值,但是模型工厂允许我们使用 Faker 生成一些假数据,这样每次生成新模型时测试数据都不一样。

所以对于商品,我们可以使用 numerify 之类的东西来生成不同的商品编号。 这将生成一个以 ABC 开头的代码,后跟三位数字,以代替散列。

 

'product_code' => $faker->numerify('ABC###')

 

如果我们需要要确保商品编码唯一,该怎么办? Faker 有一个 unique 方法,可以确保生成的内容不在表中存在。

 

'product_code' => $faker->unique()->numerify('ABC###')

对于标题,我们可以使用 words 方法来生成一些单词。 如果您想要一个单词数组,则只需要声明您想要多少个单词,但是当我们想要一些产品文本时,我们将 true 添加为第二个参数。

'title' => $faker->words(3, true)

对于价格,我们可以使用 randomNumber 方法,但是作为货币,我们可能希望保留数字小数点后两位,因此我们将使用 randomFloat 方法。 我们还需要限制最小值和最大值,因此我们可以将它们作为下两个参数传递。

 

'price' => $faker->randomFloat(2, 10, 100)

对于商品描述,我们可以再次使用 words 方法并将其长度设置为更大的值,也可以使用 paragraph,但是可以使用 realText 获得一些看起来更逼真的文本。 我们需要设置所需字符的最大长度。 可以说,在我们的情况下,最多 200 个字符是可以的。

'description' => $faker->realText(200)

最后,对于品牌,我们可以使用比单词或真实文本更有趣的东西,称为流行短语。

'tagline' => $faker->catchPhrase

这是我们更新的 ProductFactory。

use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(App\Product::class, function (Faker $faker) {
    return [
        'product_code' => $faker->unique()->numerify('ABC###'),
        'title' => $faker->words(3, true), 
        'price' => $faker->randomFloat(2, 10, 100), 
        'description' => $faker->realText(200),
        'tagline' => $faker->catchPhrase
    ];
});

因此,现在我们有了模型工厂,可以通过模型工厂助手来更新测试以使用它。

$product = factory(\App\Product::class)->make();
$response = $this->post(route('products.store'), $product->toArray());

$response->assertSuccessful();

在此示例中,有两点需要注意。

 

首先,我们使用 factory()->make() 而非 factory()->create()。它们可能听起来很相似,但是 make 将创建一个新的模型供你在测试中使用,而 create 将创建它并将其持久化到你的数据库中。如果 product 的代码中有唯一性验证,使用 create 可能会导致问题,即当数据库中已经有对应 product 时 create 操作会失败。

 

第二点需要注意的是 $this->post() 期望第二个参数是一个数组,因此我们必须在末尾使用 $product->toArray() 方法把它从对象转换成数组。

工厂状态

这似乎达到了我们的要求,但是现在我们想给 product 加一个标记表示它缺货了。将字段添加到数据库后,我们可以使用新字段更新 product 工厂。

'out_of_stock' => $faker->boolean()

这个方法会把缺货标记随机设置为 true 或 false。

 

但是,如果我们想创建一种总是缺货的 product 怎么办?一种方法是在测试中使用工厂助手时覆盖该值。

 

$product = factory(\App\Product::class)->make(['out_of_stock' => true]);

 

如果我们想使代码更具可重用性,我们可以在工厂中创建一个状态。 state 方法将模型设置为第一个参数,将状态名称设置为第二个参数,将要覆盖的值设置为第三个参数。

$factory->state(\App\Product::class, 'out_of_stock', [
    'out_of_stock' => true
]);

 

在我们的测试中,我们可以调用工厂,然后在调用 make () 之前应用状态。

 

$product = factory(\App\Product::class)->states('out_of_stock')->make();

工厂模式真正强大的地方在于当我们在工厂中具有多个状态时,它们可以同时应用。 例如,如果我们有一个状态表示某个 product 是免费的,我们可以覆盖它的价格。

$factory->state(\App\Product::class, 'free', [
    'price' => 0.00
]);

所以,当我们想要一个缺货且免费的 product 时,在测试中可以对它同时应用这两种状态。

 

$product = factory(\App\Product::class)->states(['out_of_stock', 'free')->make();

 

制作多个模型

 

另一个小提示,假如我们想要 10 个 product 而不是 1 个,我们不需要调用 10 次工厂,只需要在工厂助手中加一个数量作为第二个参数,它就会自动为我们生成 10 个 product。

 

$products = factory(\App\Product::class, 10)->make();

 

工厂中的关系

 

我们对 product 的测试进行得很顺利,但是现在我们有一个属于某个 category 下的产品。工厂模式允许我们使用另一个工厂来测试关系。

 

将 category 表添加到数据库中并定义 product 和 category 模型的关系后,我们就可以建立 category 工厂。

 

php artisan make:factory CategoryFactory --model=Category

 

为简单起见,category 只有标题和描述两个字段,因此我们可以用 word 生成标题,用 realText 生成描述。

 

use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(App\Category::class, function (Faker $faker) {
    return [
        'title' => $faker->word,
        'description' => $faker->realText(100)
    ];
});

 

现在,我们可以将 category_id 添加到 product 工厂,但是如何将 product 和 category 关联起来?

如何通过 Factories 创建多条数据

 

开发中,我们可能需要获取多条数据,这时候可以通过传递 factory 第二个参数来实现。下面是例子:

 

$products = factory(\App\Product::class, 10)->make();

 

如何通过 Factories 创建有关联的数据

 

现在产品数据的产生已经没什么问题了,但是现在我们想要获取某个分类下的数据,要怎么办呢?在 Factories 中我们可以使用另一个工厂类来达到数据关联的效果。

 

为了获取某分类下的产品,我们需要一个 category 工厂类来创建相应的 category 数据。我们可以通过以下命令创建对应的工厂类:

 

php artisan make:factory CategoryFactory --model=Category

 

为了演示方便,我们的 category 只需要标题和描述两个字段,我们可以用 $faker->word 创建标题数据,用 $faker->realText(100) 来创建描述数据。

 

use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(App\Category::class, function (Faker $faker) {
    return [
        'title' => $faker->word,
        'description' => $faker->realText(100)
    ];
});

 

现在我们已经通过 category 工厂类来产生分类数据,但是要如何将产品和分类数据关联呢?

 

我们可以使用刚在 Product factory 中创建的工厂,而不是像其他字段那样使用 faker。

 

use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(App\Product::class, function (Faker $faker) {
    return [
        'product_code' => $faker->unique()->numerify('ABC###'),
        'title' => $faker->words(3, true), 
        'price' => $faker->randomFloat(2, 10, 100), 
        'description' => $faker->realText(200),
        'tagline' => $faker->catchPhrase,
        'out_of_stock' => $faker->boolean,
        'category_id' => factory(\App\Category::class)
    ];
});

 

它知道它需要 ID,因此你不需要使用 factory()->create() 或者 factory()->create()->id 获取类别 ID。

 

现在,当你运行创建产品的测试时,它知道该产品应该属于某个类别并为你创建类别,而无需你在测试中做任何额外的操作。

 

如果对于特定的测试场景,你想创建属于某个类别的产品,则可以先定义类别,然后覆盖产品上的类别 id,使其成为你刚刚创建的类别。

 

$category = factory(\App\Category::class)->create();

$products = factory(\App\Product::class, 10)->create(['category_id' => $category->id]);

 

然后就可以断言该类别有 10 个产品

 

$this->assertEquals(10, $category->fresh()->products->count());

 

希望本文能为你提供一些关于工厂测试的想法,帮你开始在测试中使用工厂,并使将来编写测试变得更加容易。

 

更多学习内容请访问:

腾讯T3-T4标准精品PHP架构师教程目录大全,只要你看完保证薪资上升一个台阶(持续更新)​zhuanlan.zhihu.com图标

 

posted @ 2020-04-28 19:42  八重樱  阅读(1406)  评论(0编辑  收藏  举报