How can I reset a factory_girl sequence?
Asked Answered
D

9

31

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)

Divertimento answered 29/7, 2010 at 2:53 Comment(1)
Hi one can tell me where did you add factories.rb file. In features file or in spec file.Formalize
S
11

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...

Slump answered 7/1, 2011 at 6:32 Comment(2)
Hi any one can tell me where can I add factories.rb file. In features file or in spec file.Formalize
See @brandon's post below for an answer with higher votes and no monkey-patching required.Takin
C
46

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.

Ctenophore answered 9/12, 2011 at 17:34 Comment(1)
Thanks it works. I added this code in my spec->support->reset.rb file and I add After do FactoryGirl.reload end this in env.rb fileFormalize
M
13

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

Meridel answered 4/1, 2019 at 2:10 Comment(1)
Note that this has been changed to FactoryBot::Internal.sequence_by_name() github.com/thoughtbot/factory_bot/issues/1481Bareheaded
S
11

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...

Slump answered 7/1, 2011 at 6:32 Comment(2)
Hi any one can tell me where can I add factories.rb file. In features file or in spec file.Formalize
See @brandon's post below for an answer with higher votes and no monkey-patching required.Takin
E
9

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

Edelman answered 3/2, 2014 at 10:12 Comment(0)
C
5

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 "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"

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"
Catholic answered 2/11, 2011 at 5:52 Comment(3)
I find it very useful to reset sequences between tests, thus I now exactly what to expect on each and my tests are more declarative.Polygamy
How can you guarantee that a category exists with a name of "Category 1" if you don't reset the sequence that gives the name of "Category 1"?Caesium
The article you link makes it much more explicit than your summary here: if the test depends on a specific value, then it should explicitly pass that value to the factory.Caesium
R
2

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

Roos answered 3/11, 2014 at 3:31 Comment(1)
This can be simplified to sequence(:picture, 0) { |n| "image#{n % 8 + 1}.png" }Ultrared
T
1

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/

Thoreau answered 29/7, 2010 at 16:31 Comment(3)
Hello, I've been lurking their source as well and also found that post you linked. Haven't exactly figured out how to solve my issue yet. Factory.sequences behaves [apparently] in a weird way. Factory.build(:project) #=> project 1 Factory.build(:project) #=> project 2 Factory.sequences.delete(:project) Factory.build(:project) #=> project 3 Still trying to figure out why.Divertimento
The sequence is giving you a guarenteed unique ID here, not the count of the objects. That behavior is exactly what you should expect. These are used to make sure a name is generated uniquely every time you make an instance from the factory.Thoreau
That's exactly what I'm trying to reset :)Divertimento
F
0

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
Fed answered 2/7, 2014 at 10:28 Comment(1)
In version 4.5.0, I managed to do it using FactoryGirl.configuration.sequences[:iterator].instance_variable_get("@value").instance_variable_set("@value", 1) (where ':iterator' is the name of the sequence). Note that this only works on global sequences.Ake
N
0

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.

Nectarine answered 26/9, 2017 at 20:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.