Fixtury

Fixtury aims to provide an interface for creating, managing, and accessing test data in a simple and on-demand way. It has no opinion on how you generate the data, it simply provides efficient ways to access it.

Often, fixture frameworks require you to either heavily maintain static fixtures or generate all your fixtures at runtime. Fixtury attempts to find a middle ground that enables a faster and more effecient development process while allowing you to generate realistic test data.

For example, if a developer is running a test locally in their development environment there's no reason to build all fixtures for your suite of 30k tests. Instead, if we're able to track the fixture dependencies of the tests that are running we can build (and cache) the data relevant for the specific tests that are run.

require "fixtury/minitest_hooks"

class MyTest < ::Minitest::Test
  prepend ::Fixtury::MintestHooks

  fixtury "users/fresh", as: :user

  def test_whatever
    assert_eq "Doug", user.first_name
  end

end

Loading this file would ensure users/fresh is loaded into the fixture set before the suite is run. In the context of Minitest::Test, the Fixtury::MinitestHooks file will ensure the database records are present prior to your suite running. If you're using ActiveRecord use_transactional_fixtures and use_transactional_tests will be respected.

Configuration

If you're using Rails, you can require "fixtury/railtie" to accomplish a standard installation which will observe common rails files for changes and expects fixture definitions to defined in test/fixtures. See the railtie class for details.

For non-rails environments or additional configuration, you can open up the Fixtury configuration like so:

::Fixtury.configure do |config|
  config.locator_backend = :global_id # the locator behavior to use for finding fixtures
  config.filepath = File.join(root, "tmp/fixtury.yml") # the location to dump the fixtury references
  config.add_fixture_path = File.join(root, "fixtures/**/*.rb")
  config.add_dependency_path = File.join(root, "db/schema.rb")
end

See Fixtury::Configuration for all options.

When your Fixtury is configured, you should call Fixtury.start.

For minitest integration, you should dump the configuration file after the suite runs or after your fixture dependencies are built:

::Minitest.after_run do
  ::Fixtury.configuration.dump_file
end

In a CI environment, we'd likely want to preload all fixtures to produce a database snapshot to be shared. This can be done by configuring Fixtury, calling Fixtury.start, then calling Fixtury.load_all_fixtures. All fixtures declared in the configuration's fixture_paths will be loaded.

Defining Fixtures

There are two primary principals in Fixtury: namespaces and fixture definitions. See below for an example of how they're used.

Fixtury.define do

  fixture "user" do
    User.create(...)
  end

  namespace "addresses" do
    fixture "sample" do
      Address.create(...)
    end
  end

  namespace "user_with_address" do
    fixture "user", deps: "address" do |deps|
      User.create(address_id: deps.address.id, ...)
    end

    fixture "address" do
      Address.create(...)
    end
  end
end

As you can see fixtures are named in a nested structure and can refer to each other via dependencies. See Fixtury::Dependency for more specifics.

Isolation Levels

Isolation keys enable groups of fixtures to use and modify the same resources. When one fixture from an isolation level is built, all fixtures in that isolation level are built. This allows multiple fixtures to potentially mutate a resource while keeping the definition consistent.

Fixtury.define do
  namespace "use_cases" do
    namespace "onboarded", isolate: true do

      fixture "user" do
        User.create(...)
      end

      fixture "profile", deps: "user" do |deps|
        profile = Profile.create(user: deps.user, ...)
        user.update(profiles_count: 1, onboarded_at: Time.current)
        profile
      end

    end
  end
end

ActiveRecord Integration

When installed with the railtie, a MutationObserver module is prepended into ActiveRecord::Base. It observes record mutations and ensures a record is not mutated outside of the declared isolation level. If you're not using ActiveRecord check out Fixtury::MutationObserver to see how you could hook into other frameworks.