Introducing Brood: Test-Fixture Object Canisters

I have published a new Ruby library to GitHub and Rubygems: brood.

Brood lets you define object canisters (modular sets of test-fixtures) in your test setup: objects are named using an array syntax.

In your application’s test setup, you can define various broods that may be relevant to different portions of your application domain, and re-use the broods across your test suite. The named objects can anchor discussions around feature development, for example by asking questions like: “How does the introduction of feature X impact user Y?”, assuming that user Y is a brood-instantated object.

Brood depends on the gem fabrication to generate objects: you define Fabricators as normal, but instantiate specific objects within broods, thus providing some structure to your factories. Another benefit is that you can pass a block when retrieving an object in order to access the object in a threadsafe manner, which may be useful if you parallelize your application’s test suite using multithreading.

The idea for Brood comes from the article “Object Mother” by Martin Fowler: https://www.martinfowler.com/bliki/ObjectMother.html

See brood’s own minitest-based test suite for a complete example.

Basic pseudo-example:

class Department
  attr_accessor :id, :name, :users
end

class User
  attr_accessor :id, :name, :department, :counter
end

# Define fabricators for models at spec/fabricators/*_fabricator.rb.
Fabricator(:department) do
  id { sequence(:id) }
  name
  users
end
Fabricator(:user) do
  id { sequence(:id) }
  name { Faker::Name.name }
  department
end

def load_department_brood
  @brood = Brood.new
  gizmos = @brood.create([:department, :gizmos], {name: "Gizmos"}) # => Department instance
  @brood.create([:user, :bar], {id: 12, name: "Bar"}) # => User instance
  @brood.create([:user, :baaz], {name: "Baaz"}) # => User instance

  # Skip persistence (calls Fabricate.build):
  @brood.build([:user, :foobar], {name: "Foobar"}) # => User instance

  # Pass a block to customize the object (forwarded to the Fabricator block argument):
  @brood.create([:user, :baar], {name: "Baar"}) do |user|
    user.department = gizmos
  end
end

def some_test
  @brood.get([:user, :bar]) # => User instance
  @brood.get([:user, :baaz]) # => User instance
  @brood.get([:user, :quux]) # raises Brood::ObjectNotFoundError
  @brood.get([:bogus, :bar]) # raises Brood::UnknownObjectTypeError

  # Pass a block to customize and lock the object:
  @brood.get([:user, :bar]) do |user|
    counter = user.counter
    sleep 0.0001
    user.counter = counter + 1
  end # => User instance
end