Introducing the floor_manager plugin
One of the decisions a modern development team has to make is: What kind of fixture framework should we use? By now, I hope that YAML fixtures have failed you…
In this blog post, I will tell you about the answer I’ve found and why I think it is useful. But please, do not think that mine is the only answer in the field. Look for factory_girl and others.
floor_manager
As of today, I’ve released the floor_manager gem, which you can install in the usual manner. To be able to tell you how to create fixtures using that gem, here’s a few datamapper models that I’ll be creating a fixture for:
class Person
has n, :accounts
has n, :groups
property :full_name, String
property :age, Integer
end
class Account
belongs_to :person
belongs_to :primary_group, model: 'Group'
property :name, String, key: true
end
class Group
belongs_to :person
property :name, String, key: true
end
This is datamapper syntax for Ruby 1.9. If someone translates this into ActiveRecord, the following examples will all work.
Let’s create a series of fixtures for these three models. Incidentally, they will serve to illustrate almost all aspects of floor_manager.
Factory floors
The line is: “floor_manager doesn’t just handle individual girls, it handles entire factory floors. And it is even less sexistic, since it handles employees, not just ‘girls’”. You say: yes, but what does it mean?
A floor is a namespace of fixture objects. Such a floor contains employees, individual fixture templates or singletons. A really simple floor looks like this:
FloorManager.define(:my_simple_floor) do
one :employee do
name 'John Doe'
end
end
This defines a singleton fixture (more about that later) named :employee, an
instance of the Employee
class. This employees name field is then
set to ‘John Doe’.
Here’s how you use :employee in your specs:
describe "some spec" do
let(:floor) { FloorManager.get(:my_simple_floor) }
let(:employee) { floor.employee }
end
This gives you an instance of Employee (imagine this is also a model you have)
with ‘name’ filled in, but not saved. In fact, it is aequivalent to calling
floor.build(:employee)
. Here are some other ways to get objects
from the floor:
floor.create(:employee) # => a saved model, provided it could be saved.
floor.build(:employee) # => build, but don't persist, see above
floor.attrs(:employee) # => just an attribute hash
As you can see from the above code, floor_manager forces you to name your fixture spaces. It also encourages you to start new fixture spaces per topic. Or maybe even one per spec?
Floor definitions go into normal ruby files that you keep below spec/support
. Or anywhere else. You will then require these files
in your spec_helper.rb
.
Simple fixtures: Singletons
What does the term singletons mean? It refers to what you already do when you use YAML fixtures: Singletons only ever exist once in memory and also only once in the database, if they are created or saved.
Here’s a floor with a singleton:
FloorManager.define(:singleton) do
one :john_doe, model: Person do
full_name 'John Doe'
end
end
When you use floor.create(:john_doe)
in your specs, you will
trigger one database INSERT and then subsequently just access the instance you
generated. This can be helpful when you really want just one
john_doe in all your tests, let’s say he’s your system administrator and you
want to test with :john_doe
specifically, not just a john doe.
Generators
Let’s say you define the floor :generators
as follows:
FloorManager.define(:generators) do
any :person do
full_name 'John Doe'
end
end
When you now access :person
from the floor through any of the
methods on floor (#create
, #build
,
#attrs
) you will get a new instance every time. This means that
each #create
will trigger an INSERT, if the created model is
valid.
Generators1 often need to create random integers or strings for each of their instances. Here’s how you would do that:
FloorManager.define(:generators_random) do
any :person do
full_name.string(8)
age.integer(21..50)
end
end
I guess that is pretty self-explanatory. This gives you any kind of person that is legally of age but not yet retired.
Note that random generation is not one of floor_managers main strengths. Sorry. Use this:
any :person do
full_name { ... create a name here }
end
This should get you where you would need to be to use any of the available pseudo-data generation libraries.
Associations
Here’s how you would create a Person instance with Account and Group attached:
FloorManager.define(:full_example) do
one :person do
full_name 'John Doe'
age.integer(20..50)
groups.append :group
accounts.append :account
end
one :group do
name.string(8)
end
one :account do
name.string(8)
primary_group.set :group
end
end
Collections have the #append
method for adding objects to them,
methods that accept a single object should be filled with #set
.
When I now call
person = floor.create(:person)
I will get a Person instance that has a group and an account attached. The above statement yanks an entire object graph into existence! I think that is pretty cool.
If you’re asking yourself why I didn’t use any
in the above
example, please think it through and post the answer in the comments. I’ll
post the real reason about next week…
Setup, Teardown: Caveats
Like all good things, floor_manager has a few caveats attached, things to remember.
Before you can use fixtures (any kind of fixtures, really), you will need to reset your database to a known state. I mostly prefer this to be an empty database, best achieved with database_cleaner. Have a look at that project and install this properly, then you can forget about floor_manager preconditions.
When you don’t remember this, what will happen is the following: You’ll
eventually create an object that has some kind of unique constraint (like
Account
and Group
above) twice. The second time
around, the INSERT fails. You get an error message that looks like it’s coming
from floor_manager, but really is issued by your ORM system. And the insert
fails. And you can’t execute your spec. This means a spec fails because you
have a side effect via the database from an earlier spec, something you
absolutely want control over/ never to happen. So run already, use
database_cleaner, in any of its
modes!
Also, since FloorManager retains some (global :() state, you will need to
call FloorManager.reset
before each spec. This is best achieved
with the following bit in spec_helper.rb:
Rspec.configure do |config|
# other stuff...
config.before(:each) { FloorManager.reset }
end
Works With
- Rails & ActiveRecord
- Sinatra & Datamapper
- Probably a lot of other stuff: It is written to be flexible and environment-agnostic.
Why you should use floor manager
Because it gives you
- fixtures as code
- separation of fixtures
- access to attributes and unsaved objects
- flexibility between generator and singletons
- yanks entire object graphs into existence!
So
$ gem install floor_manager
right now! The mgmt.
1 Generators are what factory_girl calls a ‘factory’. The factories of floor_manager build bigger items.