Skip to content

Shopify/fixture_factory

Repository files navigation

FixtureFactory

FixtureFactory is an attempt to merge concepts from Rails Fixtures and Factory Bot Factories to bridge the gap between factories and fixtures.

Fixtures

Fixtures are fast, simple, and an easy way to seed your test database with sample data. Codebases that leverage fixtures often have faster test runs than factory-based test suites. This is due to fixtures being loaded once whereas factories build models for each test. Fixtures are also officially recommended over factories by the Rails core team.

Fixtures start to become a pain when you need to test models with complex state. Unlike factories, there's no simple way to ask for a model, transform it, and start using it in one line (without using a helper method or similar).

For more information on fixtures, check out the Rails guides.

Factories

Factories make testing any model nearly painless. State differences are expressed in factory definitions through traits, callbacks, and transient attributes. Factories are often paired with data generating libraries to add an extra degree of verification to your tests.

As mentioned above, factories' biggest downside is the speed trade-off of building models for each test. There are also added compatibility concerns with factories as they are not the officially recommended way of testing Rails apps.

For more information on factories, check out FactoryBot's wiki.

Fixture Factories

Fixture Factories is not a fixture replacement. Rather, they are meant to compliment an existing suite of fixtures. When testing models with complex state, a fixture factory can:

  • Provide a simple creation syntax that supports overrides
  • Use existing fixtures as templates for fixtures factories
  • Act as an alternative to redundant fixture definitions

Definition

FixtureFactory definitions can be made by anything that includes FixtureFactory::Registry. Typically, this is a test case class. Definitions are made with the .define_factories and .factory method:

class AccountTest < ActiveSupport::TestCase
  define_factories do
    factory(:account)
    factory(:enterprise_account, class: -> { Account }) do
      { plan: :enterprise }
    end
  end
end

Naming

The name of your fixture is important. It infers the class (with ActiveSupport::Inflector) you'll be using, and the fixture method you source fixtures from. Fixture with non-standard names can get around this problem 2 different ways:

  1. Define a base fixture with a simple name and extend via parent:
class RecipeTest < ActiveSupport::TestCase
  define_factories do
    factory(:recipe) # infers "Recipe" and "recipes"
    factory(:cake_recipe, parent: :recipe) do
      { name: "Cake" }
    end
  end
end
  1. Use the class and via options to specify class and fixture method:
class RecipeTest < ActiveSupport::TestCase
  define_factories do
    factory(:cake_recipe, class: -> { Recipe }, via: :recipes) do
      { name: "Cake" }
    end
  end
end

Fixtures

The whole point of fixture factories is to complement a fixture suite. Typically, you'll want to link your factories to fixtures. This is done with the via and like options:

class UserTest < ActiveSupport::TestCase
  define_factories do
    factory(:user, like: :bob)
    factory(:admin_user, class: -> { User }, like: :bob, via: :users) do
      { role: :admin }
    end
  end
end

Inheritance

There are two aspects of inheritance to factories definitions. Inheritance at the registry level, and inheritance at the definition level. Registry subclasses inherit definitions from their superclass. Fixture factory definitions can specify a parent factory to inherit options from. Here's an example:

class ActiveSupport::TestCase
  define_factories do
    factory(:address)
  end
end

class AddressTest < ActiveSupport::TestCase
  define_factories do
    factory(:primary_address, parent: :address) do
      { primary: true }
    end
  end
end

Sequences

Factories often don't play well with uniqueness constraints. If you need to generate unique values in your factories, consider using sequences. An auto-incrementing number is passed to every factory definition block which can be used to seed unique values.

class ArticleTest < ActiveSupport::TestCase
  define_factories do
    factory(:article) do |count| # starts at 1
      { title: "Unique Article", slug: "article-#{count}" }
    end
  end
end

Usage

FixtureFactory usage is easiest in registries that include FixtureFactory::Methods. This exposes several methods that gives your tests superpowers.

attributes_for(name, overrides = {})

Generates a hash of attributes given a factory name and an optional hash of override attributes.

class UsersControllerTest < ActionDispatch::IntegrationTest
  define_factories do
    factory(:user) do
      { email: 'user@example.com', admin: false }
    end
  end
  setup do
    @user_attributes = attributes_for(:user, admin: true)
    # => { email: "user@example.com", admin: true }
  end
end

attributes_for_list(name, count, overrides = {})

Generates an array of hash attributes given a factory name, a count, and an optional hash of override attributes.

class BooksControllerTest < ActionDispatch::IntegrationTest
  define_factories do
    factory(:book) do
      { title: 'Ruby Under a Microscope' }
    end
  end
  setup do
    @book_attributes = attributes_for_list(:book, 2, title: "Why's Poignant Guide to Ruby")
    # => [{ title: "Why's Poignant Guide to Ruby" }, { title: "Why's Poignant Guide to Ruby" }]
  end
end

build(name, overrides = {})

Generates an unpersisted instance of a model given a factory name and an optional hash of override attributes.

class CourseTest < ActiveSupport::TestCase
  define_factories do
    factory(:course) do
      { name: 'Rails for Zombies' }
    end
  end
  setup do
    @course = build(:course, name: 'Ruby Monk')
    # => #<Course:0x000 name: "Ruby Monk">
  end
end

build_list(name, count, overrides = {})

Generates an array of unpersisted model instances given a factory name and an optional hash of override attributes.

class PostTest < ActiveSupport::TestCase
  define_factories do
    factory(:post) do
      { title: 'Rails 5.2' }
    end
  end
  setup do
    @post = build_list(:post, 2, title: 'Rails 6')
    # => [#<Post:0x000 id: nil, title: "Rails 6">, #<Post:0x000 id: nil, title: "Rails 6">]
  end
end

create(name, overrides = {})

Generates a persisted model instance given a factory name and an optional hash of override attributes.

class CommentTest < ActiveSupport::TestCase
  define_factories do
    factory(:comment) do
      { content: 'Hello World!', post: build(:post) }
    end
  end
  setup do
    @comment = create(:comment, post: create(:post, title: 'Wow'))
    # => #<Comment:0x000 id: 1, title: "Hello World!", post_id: 1>
  end
end

create_list(name, count, overrides = {})

Generates an array of persisted model instances given a factory name and an optional hash of override attributes.

class BlogTest < ActiveSupport::TestCase
  define_factories do
    factory(:blog) do
      { name: 'Giant Robots Smashing Into Other Giant Robots' }
    end
  end
  setup do
    @blog = create_list(:blog, 2, title: 'Riding Rails')
    # => [#<Blog:0x000 id: 1, title: "Riding Rails">, #<Blog:0x000 id: 2, title: "Riding Rails">]
  end
end