Skip to content

Factory Sequence: bug-fix + new methods for setting, generating and rewinding #1742

Merged
neilvcarvalho merged 3 commits intothoughtbot:mainfrom
CodeMeister:sequence-uri
Jun 2, 2025
Merged

Factory Sequence: bug-fix + new methods for setting, generating and rewinding #1742
neilvcarvalho merged 3 commits intothoughtbot:mainfrom
CodeMeister:sequence-uri

Conversation

@CodeMeister
Copy link
Contributor

Summary

I found an odd bug when inheriting sequences that don't have a block. With no block to compare, determining if it was an inherited sequence was problematic.

The solution was to give each sequence it's own unique reference (URI).

Once this was in place, it became an easy process to expand :generate, generate_list and :rewind_sequences to target factory sequences, not just the global ones as it does now.

Being able to generate factory sequences makes testing much easier, without have to build multiple objects just to increment the sequence!

It also became a simple process to allow the sequence to be set to a different value, either for testing or from the console.

Full tests & documentation are included.

BugFix

factory :parent do
  trait :with_counter do
    sequence :counter
  end
  
  factory :child
end

build :child, :with_counter
#=> NoMethodError

When a factory trait that includes a sequence with no block is included in a child factory build, an exception is raised.

To solve this, each sequence now includes a list of URIs that identify it. (the multiple URIs allow for factory and trait aliases)

There is no change to the existing user API.

Sequence URIs

Each URI is composed of up to three names:

position name required
1. factory name: if - the sequence is defined within a Factory or a Factory Trait
2. trait name: if - the sequence is defined within a Trait
3. sequence name: always required

Here's a complete example showing the URIs, by calling generate for the matching sequence:

FactoryBot.define do
  sequence(:sequence) {|n| "global_sequence_#{n}"}
  # generate(:sequence)

  trait :global_trait do
    sequence(:sequence) {|n| "global_trait_sequence_#{n}"}
    # generate(:global_trait, :sequence)
  end

  factory :user do
    sequence(:sequence) {|n| "user_sequence_#{n}"}
    # generate(:user, :sequence)

    trait :user_trait do
      sequence(:sequence) {|n| "user_trait_sequence_#{n}"}
      # generate(:user, :user_trait, :sequence)
    end

    factory :author do
      sequence(:sequence) {|n| "author_sequence_#{n}"}
      # generate(:author, :sequence)

      trait :author_trait do
        sequence(:sequence) {|n| "author_trait_sequence_#{n}"}
        # generate(:author, :author_trait, :sequence)
      end
    end
  end
end

You can get the full details in the updated docs.

Generating sequences & lists

generate and generate_list work as normal. Following the URI definition above, a global sequence URI is simply it's name.

Factory sequences are generated using their URI: generate(:my_factory, :sequence)

Rewinding sequences

FactoryBot.rewind_sequences is untouched and still rewinds all sequences.

'FactoryBot.rewind_sequence(URI)` is a new method that will rewind the targed sequence:

FactoryBot.rewind_sequence(:user, :email)

Setting the sequence value

FactoryBot.set_sequence(URI, new_value) is a new method that allows the sequence to be set to a new value, provided:

  • the given value matches the sequence type:
    • can't set an Integer sequence to a String
  • the given value is contained within the sequence:
    • can't set a sequence of names to a name not already present.
generate(:user, :email) #=> "user_1@example.com"
FactoryBot.set_sequence(:user, :email, 10_395)
generate(:user, :email) #=> "user_10395@example.com"

Logic

  1. If it's an Integer sequence, and the new value is an integer, set it directly.

  2. If it responds to :find_index, use it because it is faster & doesn't change the sequence if the value is not found

  3. For all other sequences: rewind the sequence, then keep incrementing until a match is found or the search times-out. Reset the original value if not found.

The timeout defaults to 3 seconds, but can be changed with an ENV variable.

Internal helper methods for developers

FactoryBot::Sequence.find(URI) will return the sequence that matches the given URI, checking both global and inline sequences.

FactoryBot::Sequence.sequence_setting_timeout will return the number of seconds allowed for setting a sequence to a new value.

Copy link
Member

@neilvcarvalho neilvcarvalho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this awesome contribution! It seems correct as far as I've read, though I'm still going to test more thoroughly later.

The comments are mostly supplementary to the code - specs and documentation. I reviewed it in a few batches and ended up adding lots of them 😅 I can take over if you'd like.

@CodeMeister
Copy link
Contributor Author

Hi @neilvcarvalho, happy for you to take over and make any changes you want. 👍

@CodeMeister
Copy link
Contributor Author

Hi @neilvcarvalho, I've seen the comments about the sequence bug. Is there anything I can do to help speed this up for you? 🏎️

CodeMeister and others added 3 commits June 2, 2025 09:04
:generate and :generate_list expanded to work with factory sequences:
  - generate(:sequence_name)
  - generate(:factory_name, :sequence_name)
  - generate_list(:sequence_name, 3)
  - generate_list(:factory_name, :sequence_name, 3)

:rewind_seqence added to rewind individual sequences
  - rewind_sequence(:factory_name, :trait_name, :sequence_name)

:set_sequence added to set the sequence to a new value:
  - set_sequence(:sequence_name, new_value)
  - set_sequence(:factory_name, :sequence_name, new_value)

Test coverage at 100% & docs updated.
When calling :peek on the Enumerator class, internally it calls :next.
There was an issue when stubbing the :next method to raise an exception, then
triggering it when calling :peek.

This was only an issue in Truffleruby.
@neilvcarvalho
Copy link
Member

@CodeMeister Thanks for offering help. I'm finished with my changes and I'm going to merge this PR soon.

@neilvcarvalho neilvcarvalho merged commit 0abe26c into thoughtbot:main Jun 2, 2025
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants