2009 m. lapkričio 21 d., šeštadienis

Blueprints - factories and fixtures replacement for lazy typists

Why another replacement?

What is your least favorite part of testing? For me it's always been generating data for test. Long before factories were born we all were using fixtures, however they had some real limitations that I'm sure you all know about. Someone got fed up with fixtures and factories were born. They've overcome most annoyances about fixtures but introduced some new ones, namely much slower speed of tests and repetition in your tests when creating data. Of course lazy typists didn't like this approach and so the new wave (really small one) was born - using scenarios. It was hornsby scenarios plugin that introduced it, however it wasn't actively developed and was pretty archaic.
One day my team got hands on it, they fixed some things that were most annoying and made hornsby pretty usable. However too many improvements were born in our heads so we took some code from hornsby, deleted most of unnecessary code, improved the concept itself and introduced a new plugin/gem - blueprints.

What is Blueprints?

Blueprints is a mix of fixtures, factories, rake and some new concepts. Imagine a blueprint as a scenario that defines how and what object(s) is built/modified. Let's say we need an apple, here's how it would look like in blueprints (spec/blueprint.rb):

blueprint :apple do
@apple = Fruit.create!(:species => 'apple', :color => 'green')
end
And in your test file you could do:

it "should be green apple" do
build :apple
@apple.species.should == "apple"
@apple.color.should == "green"
end
Now of course I'm assuming that you have Fruit as an ActiveRecord model in your application.

This was easy, wasn't it? Blueprints can also depend on another blueprints. So for example if you want to have apple worm that eats apple, you could write this:

blueprint :apple_worm => :apple do
@apple_worm = Worm.create!(:name => 'wormie')
@apple_worm.eat(@apple)
end
Note that we have @apple instance variable in this blueprint since we added a dependency on :apple blueprint for it. Now you can

build :apple_worm
in your test and you have both - @apple and @apple_worm.

This is pretty much how original hornsby scenarios did look like (except for several annoying bugs). Now here's what we improved:
  1. Take another look at :apple blueprint, see the repetition? If you don't - check the name of blueprint and the name of instance variable (both are named apple), we took out the repetition, so whatever block of blueprint evaluates to is assigned to instance variable with the same name as blueprint. This means we could rewrite our :apple scenario to this:

    blueprint :apple do
    Fruit.create!(:species => 'apple', :color => 'green')
    end
    And once you build it in test file you still have @apple. Note that this variable is not assigned if one with the same name exists (so it doesn't accidentally override some instance variable you set in another blueprint only because you named them the same).

  2. We even took one more step forward and added an option to shorten this even more. So the same apple scenario could be written like this:

    Fruit.blueprint :apple, :species => 'apple', :color => 'green'
    As we also needed some nicer way to implement dependencies, since

    Fruit.blueprint {:apple => :apple_tree}, :species => 'apple'
    looks ugly, we introduced depends_on method that could be used like this:

    Fruit.blueprint(:apple, :species => 'apple').depends_on(:apple_tree)
    There was one more issue with dependencies. As we couldn't use instance variables out of block scope, we introduced :@ syntax. So a scenario similar to :apple_worm would look like this:

    Worm.blueprint(:apple_worm, :name => 'another wormie', :apple_to_eat => :@apple).depends_on(:apple)
    Notice the colon before @apple.

  3. We also introduced another variation of blueprint method, one without the name of scenario, which for :apple blueprint could be used like this:

    blueprint :apple do
    Fruit.blueprint :species => 'apple'
    end
    Note that the difference between using Fruit.blueprint and Fruit.create! is only that .blueprint bypasses attr_protected and attr_accessible (which is usually desired when creating test data).

  4. We've also introduced prebuilt blueprints - these are available in all test cases (similar to fixtures). As blueprints are transactional, this means that these scenarios can only be built once for all test cases (again similar to fixtures). It could really cut time that tests run if you enable them for most common blueprints. For example on one project we've had two users that were used is many test cases, so I made them preloaded, and test time literally dropped in half! You can read more about these in blueprints github wiki

  5. Namespaces were also introduced in recent version of blueprints. You can also read more about those in github wiki.



Conclusion

Now you may not like blueprints due to the fact it's harder to see what test data you have, without checking additional file. Now if you're like me - you usually memorize most usually used data and so you don't need to check it. Anyway blueprints give you more advanced features than any fixtures framework I know of at the same time giving you a flexibility to decide how much data you need in all tests and how much only in some cases. It's also one of the most concise and it can be as fast as fixtures. One limitation though is that it probably doesn't support sqlite as sqlite doesn't support transactions. It's been production tested and we haven't found any bugs. So give it a try and and tell us what could be improved or if some bug slipped. You can find blueprints as well as instructions on how to use it at http://github.com/sinsiliux/Blueprints

1 komentaras:

Anonimiškas rašė...

this is interesting, thanks for the post!

unfortunately, some code samples are not fully visible... right text margin cuts them off...