gcdtech/seeding

用于实现最佳实践的种子模式设置类

2.0.0 2023-02-23 14:38 UTC

This package is auto-updated.

Last update: 2024-09-23 18:33:25 UTC


README

在我们的观点中,种子是现代开发的一个主要组件。与其说它是一种在事后用随机数据填充表格的方法,我们更认为它是一种推动开发的方式,这种方式可以解锁不同系统组件的团队。

此库为希望使用我们“场景”数据种子方法的工程提供了一些辅助类。

场景数据种子

场景数据种子创建完成软件关键旅程所需的数据。它们通过设置好状态,让用户可以加入并完成关键操作来揭示功能。这节省了时间,因为测试人员不需要自己创建这些状态。

场景对团队中的许多人都有用。

  1. QA:测试人员可以在关键位置加入复杂的旅程,而无需设置相关状态的无聊过程。QA还可能设置场景,以便在自动化测试工具中测试,以最小化测试运行时间。

  2. PMs:项目经理也对测试关键旅程感兴趣。他们还可能要求配置场景,以便与客户完成演示。

  3. 设计师:设计师通常需要在复杂旅程的中段或末端协助组件或屏幕。场景可以直接将他们带到那里,而不浪费时间去探索如何实现。

  4. 开发者:开发者经常需要重置数据以重新尝试对代码的修改。场景种子使这个过程快速且简单。

在实践中,用于使种子变得容易的辅助对象对于单元测试创建者也是非常有用的。

黄金法则

在我们的经验中,有一些黄金法则,如果每个人都遵守,将确保我们都能从数据种子中获得最大价值。

1. 命名:场景命名一个功能(或子功能)。

场景的名称应该描述“为什么”它创建,而不是“创建什么”。这个场景为什么重要?对谁重要?

2. 描述性:场景应该描述它们创建的内容

场景的主体应该详细地向控制台说明所配置的各种对象和旅程。登录应该详细说明,URL展示等。这应该考虑展示方式——选项卡、粗体等,使其尽可能易于阅读。

场景的输出应该提供用户完成场景所需的一切。

3. 一致性:场景不应该“垃圾邮件”数据库

如果你连续两次运行种子器,它不应该创建两倍数量的记录。它应该找到并重置现有记录,只有在缺少的情况下才添加。这非常重要,因为种子器预计会被反复运行。

4. 完整性:场景应该相互独立

一个场景不应该依赖于另一个场景,或者实际上依赖于执行顺序。用户应该能够运行所有场景,或者只运行一个场景,并期望它工作。

5. 隔离性:不要删除或更新其他记录

场景应该仅修改它创建的记录的数据。它不应该清空表等。在理论上,你应该能够在任何时候运行其他人的场景,而不会损坏自己的数据。

6. 组合:由共享单元构建

通过使用工厂从食谱制作对象来保持DRY(Don't Repeat Yourself)原则。

关键类

ScenarioDataSeeder

这是Rhubarb Stem模块提供的一个类,它是我们的场景的容器。

基本用法如下:

class CustomerSeeder extends ScenarioDataSeeder
{
    public function getScenarios(): array
    {
       return [
           new Scenario("Customers can't register if email address is in use", function(ScenarioDescription $description){
               // Code to create scenario data
               $description->writeLine("Something to describe what we've made");    
           }),
           new Scenario("Customers can reset their password", function(ScenarioDescription $description){
               // Code to create scenario data
               $description->writeLine("Something to describe what we've made");
           })
       ];
    }
}

我们简单地创建一个getScenarios函数,并返回一个包含Scenario对象的数组。这些对象使用一个字符串名称和一个回调函数构建,该回调函数接收一个ScenarioDescription对象。回调函数应该实现播种器的核心逻辑,然后使用描述对象来描述已配置的内容。

ConsistentScenarioDataSeeder

这是ScenarioDataSeeder的一个简单扩展,用于初始化Faker库,使得随机数据虽然随机但可预测。这允许播种器在使用随机数据的情况下也具有幂等性。

SeedingFactory

SeedingFactory是一个负责创建特征中涉及的对象的类。在项目中,每个“顶级”模型都有一个SeedingFactory并不罕见。然而,并不意味着每个模型都应该有一个SeedingFactory。

SeedingFactory中的每个公共方法都应该接受一个Recipe并返回一个新的模型。例如:

$customerSeeder->createCustomer(CustomerRecipe::create())

而不是创建新的SeedingFactory实例,你应该调用静态的::get()方法。这是一个简单的便利方法,以便你可以使播种表达式保持简短。

// Not this
$seeder = new CustomerSeeder();
$customer = $seeder->createCustomer(CustomerRecipe::create());

// Do this instead
$customer = CustomerSeeder::get()->createCustomer(CustomerRecipe::create());

Recipe

Recipe对象是一个简单的类,用于表达工厂可能需要的值、标志和开关,以控制它返回的内容。

通过再次调用::create()静态方法创建Recipe,这样表达式就可以对用户更清晰地表达。

虽然Recipe通常包含所有公共属性,但仍然常见使用流畅模式设置方法,以允许这些元素被链接在一起。例如:

CustomerSeeder::get()->createCustomer(
    CustomerRecipe::create()
        ->withName("John", "Smith"),
        ->withEmail("jsmith@gmail.com"),
        ->isArchived()
        ->hasContacts(3)
    );

流畅方法通常以'with'为前缀,但如上例所示,这可能会变化。可读性是这里的主要目标。注意,这些方法不一定只接受单个参数,特别是如果它们是成组或成对出现的话。

封装上述设置的Recipe类可能看起来像这样:

class CustomerRecipe extends Recipe
{
    public $forename;
    public $surname;
    public $email;
    public $archived = false;
    public $numberOfcontacts = 0;
    
    public function withName($forename, $surname)
    {
        $this->forename = $forename;
        $this->surname = $surname;
        
        return $this;       // Fluent style - don't forget to return $this
    }
    
    public function withEmail($email)
    {
        $this->email = $email;
        return $this;
    }
    
    public function isArchived()
    {
        $this->archived = true;
        return $this;
    }
    
    public function hasContacts($contacts)
    {
        $this->numberOfContacts = $contacts;
        return $this;
    }
}

创建播种方法

在Factory中创建播种方法时,通常遵循相同的模式

  1. 如果没有传递recipe,则创建一个空的一个
  2. 通过设置未设置的值来完成recipe的设置
  3. 查找已存在的模型,如果不存在,则创建一个新的
  4. 设置所有模型值并返回模型

为了帮助找到预存在的模型,SeedingFactory有一个有用的辅助函数findOrCreateByColumns,你可以传递模型类名称和一个必须匹配的键值对数组。

这个完整的示例可能有助于理解。

class CustomerFactory extends SeedingFactory
{
    public function createCustomer(?CustomerRecipe $recipe = null)
    {
        // 1. Make sure we have a recipe
        if (!$recipe){
            $recipe = CustomerRecipe::create();
        }
                
        // 2. Complete the recipe
        $recipe->forname = $recipe->forename ?? $this->faker->firstname;  
        $recipe->surname = $recipe->surname ?? $this->faker->lastname;  
        $recipe->email = $recipe->email ?? $this->faker->email;
        
        // 3. Find or make the object
        $customer = $this->findOrCreateByColumns(Customer::class, [
            'Forename' => $recipe->forname,
            'Surname' => $recipe->surname
        ]);
        
        // 4. Set the values and save
        $customer->Email = $recipe->email;
        $customer->Archived = $recipe->archived;
        $customer->save();
        
        // And in this case spin up some contacts too..        
        for($x = 0; $x < $recipe->numberOfContacts; $x++){        
            $this->createContact($customer, $this->faker->firstname, $this->faker->lastname);
        }
    }
    
    private function createContact(Customer $customer, $forename, $surname)
    {
        // Do something to make a contact...
    }
}

请注意,SeedingFactory提供了一个已播种的$faker属性。播种器中的任何随机元素都应该使用这个Faker实例,而不是进行自定义随机化。