How can I reset a factory_girl sequence?

2019-01-25 02:37发布

问题:

Provided that I have a project factory

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

And that I'm creating instances of project

Factory.build :project
Factory.build :project

By this time, the next time I execute Factory.build(:project) I'll receive an instance of Project with a title set to "project 3 title" and so on. Not surprising.

Now say that I wish to reset my counter within this scope. Something like:

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

What would be the best way to achieve this?

I'm currently using the following versions:

factory_girl (1.3.1) factory_girl_rails (1.0)

Thanks in advance, Best regards.

回答1:

Hey everybody, After tracing my way through the source code, I have finally come up with a solution for this. If you're using factory_girl 1.3.2 (which was the latest release at the time I am writing this), you can add the following code to the top of your factories.rb file:

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

Then, in Cucumber's env.rb, simply add:

After do
  Factory.reset_sequences
end

I'd assume if you run into the same problem in your rspec tests, you could use rspecs after :each method.

At the moment, this approach only takes into consideration sequences defined within a factory, such as:

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

I have not yet written the code to handle: Factory.sequence ...

Hope this helps all the other frustrated people out there who cannot understand why in the world factory girl doesn't provide this already. Maybe I'll fork the github project and submit a pull request with this fix since it doesn't change any of their internal functionality.

-Andrew



回答2:

Just call FactoryGirl.reload in your before/after callback. This is defined in the FactoryGirl codebase as:

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

Calling FactoryGirl.sequences.clear is not sufficient for some reason. Doing a full reload might have some overhead, but when I tried with/without the callback, my tests took around 30 seconds to run either way. Therefore the overhead is not enough to impact my workflow.



回答3:

For googling people: without further extending, just do 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"

works for me on

* factory_girl (4.3.0)
* factory_girl_rails (4.3.0)

https://stackoverflow.com/a/16048658



回答4:

According to ThoughBot Here, the need to reset the sequence between tests is an anti-pattern.

To summerize:

If you have something like this:

FactoryGirl.define do
  factory :category do
    sequence(:name) {|n| "Category #{n}" }
  end
end

Your tests should look like this:

Scenario: Create a post under a category
   Given a category exists with a name of "Category 1"
   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"

Not This:

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"


回答5:

Had to ensure sequences are going from 1 to 8 and restart to 1 and so on. Implemented like this:

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

The doc says "The value just needs to support the #next method." But to keep you CustomSequence object going through it needs to support #peek method too. Lastly I don't know how long this will work because it kind of hack into FactoryGirl internals, when they make a change this may fail to work properly



回答6:

There's no built in way to reset a sequence, see the source code here:

http://github.com/thoughtbot/factory_girl/blob/master/lib/factory_girl/sequence.rb

However, some people have hacked/monkey-patched this feature in. Here's an example:

http://www.pmamediagroup.com/2009/05/smarter-sequencing-in-factory-girl/



回答7:

To reset particular sequence you can try

# 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


回答8:

If you are using Cucumber you can add this to a step definition:

Given(/^I reload FactoryGirl/) do
  FactoryGirl.reload
end

Then just call it when needed.



回答9:

This is pretty old, but it's the top result on google for the relevant keywords. For anyone else stumbling across this.

There is a class method called sequence_by_name to fetch a sequence by name, and then you can call rewind and it'll reset to 1.

FactoryBot.sequence_by_name(:order).rewind

Or if you want to reset all.

FactoryBot.rewind_sequences

Here is the link to the file on github