如果你想为你的 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','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,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()
但是,如果我们想创建一种总是缺货的 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,'description' => $faker->realText(100)
];
});
现在我们已经通过 category 工厂类来产生分类数据,但是要如何将产品和分类数据关联呢?
我们可以使用刚在 Product factory 中创建的工厂,而不是像其他字段那样使用 faker。
use Illuminate\Support\Str;
use Faker\Generator as Faker;
$factory->define(App\Product::class,'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架构师教程目录大全,只要你看完保证薪资上升一个台阶(持续更新)