如何重置 factory_girl 的序列?

31

假设我有一个项目工厂

Factory.define :project do |p|
  p.sequence(:title)    { |n| "project #{n} title"                  }
  p.sequence(:subtitle) { |n| "project #{n} subtitle"               }
  p.sequence(:image)    { |n| "../images/content/projects/#{n}.jpg" }
  p.sequence(:date)     { |n| n.weeks.ago.to_date                   }
end

而且我正在创建项目实例

Factory.build :project
Factory.build :project

到这个时候,下一次执行 Factory.build(:project) 我将会得到一个标题为“project 3 title”的 Project 实例,以此类推。这并不奇怪。

现在假设我想要在这个作用域内重置我的计数器。类似于:

Factory.build :project #=> Project 3
Factory.reset :project #=> project factory counter gets reseted
Factory.build :project #=> A new instance of project 1

如何最好地实现这个目标?

我目前使用以下版本:

factory_girl (1.3.1) factory_girl_rails (1.0)


请问你把 factories.rb 文件加在哪里了?是在 features 文件夹还是 spec 文件夹中? - Selvamani
9个回答

46

只需在你的before/after回调中调用FactoryGirl.reload即可。这在FactoryGirl代码库中定义为:

module FactoryGirl
  def self.reload
    self.factories.clear
    self.sequences.clear
    self.traits.clear
    self.find_definitions
  end
end

调用FactoryGirl.sequences.clear是不够的,原因未知。进行完全重新加载可能会有一些额外开销,但是当我尝试使用/不使用回调时,我的测试无论如何都需要大约30秒才能运行。 因此,这种额外开销不足以影响我的工作流程。


谢谢,它有效。我将此代码添加到我的spec->support->reset.rb文件中,并在env.rb文件中添加了After do FactoryGirl.reload end - Selvamani

11

在源代码中进行了一番追踪之后,我终于找到了解决方法。如果你使用的是 factory_girl 1.3.2(这是我撰写本文时的最新版本),你可以将以下代码添加到 factories.rb 文件的顶部:

class Factory  
  def self.reset_sequences
    Factory.factories.each do |name, factory|
      factory.sequences.each do |name, sequence|
        sequence.reset
      end
    end
  end
  
  def sequences
    @sequences
  end
  
  def sequence(name, &block)
    s = Sequence.new(&block)
    
    @sequences ||= {}
    @sequences[name] = s
    
    add_attribute(name) { s.next }
  end
  
  def reset_sequence(name)
    @sequences[name].reset
  end
  
  class Sequence
    def reset
      @value = 0
    end
  end
end

然后,在Cucumber的env.rb文件中,只需添加以下内容:

After do
  Factory.reset_sequences
end

我会假设如果您在rspec测试中遇到了同样的问题,您可以使用rspec的after :each方法。

目前,这种方法只考虑在工厂内定义的序列,例如:

Factory.define :specialty do |f|
  f.sequence(:title) { |n| "Test Specialty #{n}"}
  f.sequence(:permalink) { |n| "permalink#{n}" }
end

我还没有编写处理Factory.sequence的代码...


有人可以告诉我在哪里添加factories.rb文件吗?在features文件夹还是spec文件夹中? - Selvamani
请参考下面@brandon的帖子,其中包含更高票数的答案,且无需进行猴子补丁。 - Jason

10

有一个类方法叫做sequence_by_name,可以通过名称获取序列,然后您可以调用rewind方法,它会重置到1。

FactoryBot.sequence_by_name(:order).rewind

或者,如果您想要重置所有内容。

FactoryBot.rewind_sequences

这里是在Github上的文件链接


9

如果你需要使用谷歌搜索,那么只需简单地执行FactoryGirl.reload即可。

FactoryGirl.create :user
#=> User id: 1, name: "user_1"
FactoryGirl.create :user
#=> User id: 2, name: "user_2"

DatabaseCleaner.clean_with :truncation #wiping out database with truncation
FactoryGirl.reload

FactoryGirl.create :user
#=> User id: 1, name: "user_1"

这在我的电脑上可以运行

* factory_girl (4.3.0)
* factory_girl_rails (4.3.0)

https://dev59.com/nmbWa4cB1Zd3GeqPYqLn#16048658


5
根据ThoughBot的说法(这里),在测试之间重置序列是一种反模式。 总结: 如果你有类似下面这样的代码:
FactoryGirl.define do
  factory :category do
    sequence(:name) {|n| "Category #{n}" }
  end
end

你的测试应该长这样:
Scenario: Create a post under a category
   Given a category exists with a name of "My Category"
   And I am signed in as an admin
   When I go to create a new post
   And I select "My Category" from "Categories"
   And I press "Create"
   And I go to view all posts
   Then I should see a post with the category "My Category"

不要这个:
Scenario: Create a post under a category
  Given a category exists
  And I am signed in as an admin
  When I go to create a new post
  And I select "Category 1" from "Categories"
  And I press "Create"
  And I go to view all posts
  Then I should see a post with the category "Category 1"

2
我发现在测试之间重置序列非常有用,这样我就知道每个测试的期望结果,我的测试更加声明式。 - Macario
如果不重置给予“Category 1”名称的序列,你如何保证存在一个名称为“Category 1”的分类? - Code-Apprentice
你提供的链接文章比你在这里的摘要更加明确:如果测试依赖于特定值,则应该将该值明确地传递给工厂。 - Code-Apprentice

2

需要确保序列从1到8,并重新开始为1等等。实现方式如下:

class FGCustomSequence
  def initialize(max)
    @marker, @max = 1, max
  end
  def next
    @marker = (@marker >= @max ? 1 : (@marker + 1))
  end
  def peek
    @marker.to_s
  end
end

FactoryGirl.define do
  factory :image do
    sequence(:picture, FGCustomSequence.new(8)) { |n| "image#{n.to_s}.png" }
  end
end

这份文档中提到:“该值只需要支持#next方法。”但是为了让你的CustomSequence对象始终保持,它还需要支持#peek方法。最后,我不知道这种方法能够持续多久,因为它有点侵入FactoryGirl内部,当他们进行更改时,可能无法正常工作。


这可以简化为 sequence(:picture, 0) { |n| "image#{n % 8 + 1}.png" } - Petr Beneš

1

你好,我一直在潜水研究他们的源代码,也找到了你链接的那篇帖子。我还没有完全弄清楚如何解决我的问题。Factory.sequences 的行为表现得很奇怪。Factory.build(:project) #=> project 1 Factory.build(:project) #=> project 2 Factory.sequences.delete(:project) Factory.build(:project) #=> project 3我仍在努力弄清原因。 - DBA
这里的序列为您提供了一个保证唯一的ID,而不是对象的计数。这种行为正是您应该期望的。它们用于确保每次从工厂创建实例时都会生成唯一的名称。 - Winfield
1
这正是我想要重置的 :) - DBA

0
如果您正在使用Cucumber,您可以将以下内容添加到步骤定义中:
Given(/^I reload FactoryGirl/) do
  FactoryGirl.reload
end

需要时只需调用它即可。


0

要重置特定的序列,您可以尝试

# spec/factories/schedule_positions.rb
FactoryGirl.define do
  sequence :position do |n| 
    n
  end

  factory :schedule_position do
    position
    position_date Date.today
    ...
  end
end

# spec/models/schedule_position.rb
require 'spec_helper'

describe SchedulePosition do
  describe "Reposition" do
    before(:each) do
      nullify_position
      FactoryGirl.create_list(:schedule_position, 10)
    end
  end

  protected

  def nullify_position
    position = FactoryGirl.sequences.find(:position)
    position.instance_variable_set :@value, FactoryGirl::Sequence::EnumeratorAdapter.new(1)
  end
end

1
在4.5.0版本中,我使用FactoryGirl.configuration.sequences[:iterator].instance_variable_get("@value").instance_variable_set("@value", 1)成功实现了它(其中“:iterator”是序列的名称)。请注意,这仅适用于全局序列。 - Isaac Betesh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接