I'm sitting in Sydney airport waiting for my flight back to Melbourne, so I thought I'd complete this little blog article that I've been meaning to write ever since I implemented a similar solution in a recent Ruby on Rails project.
Anyway, let us proceed ...
Creating test data can often require the construction of complex data object, usually with many dependencies that need to be created at the same time as the data object. The Builder pattern greatly simplifies the code required to create such objects, and the implementation of this pattern is especially easy in a dynamic language such as Ruby. The result is a highly customizable builder that allows your tests to create very specific and complex data structures with the minimum of code.
Note: What will be covered is a very simple Builder, simply covering the basics of the implementation to demonstrate the potential of the Builder pattern in Ruby. Different projects have different needs, and it is quite possible (and highly probable!) that a Builder in a real system will have more features built into it.
A common domain model for a system revolves around the concept of Products. Whilst a Product often has many direct attributes, such as name, description and price, it will often have many associations of either the one-to-many or many-to-many variety. Such examples include Brands, Retail Chains, Categories, etc. Using RoR's ActiveRecord pattern to save Products often involve verbose code such as:
Product.create!(:name => "Yellow Submarine", :description => "Often found in the navy",
:price => 99.99, :brand => Brand.new(:name => "ADF"),
:categories => [Category.new(:name => "Ship"),
Category.new(:name => "Submarine")]
Imagine using that syntax whenever you need to create a Product. It is simply too tedious and verbose to contemplate. The remainder of this blog will use this scenario to demonstrate an effective solution.
The final solution must present us with the following features:
Ruby's method_missing presents us with the ideal tool to implement an effective, scalable and concise Builder. A new class, Product Builder, will be required for the building of Products. Note that a Builder class is not required for all of your application's domain models; just the ones that are the root of a tree of complex associations (such as a Product, but not necessarily a Brand or Category).
Every story has to start somewhere, so let us start by defining a Product Builder class with an intializer that creates a Product internally with a set of sensible defaults:
class ProductBuilder
def initialize
default_attributes = {
:name => "Yellow Submarine",
:description => "A new addition to our toy store",
:price => 99.99,
:brand => Brand.new(:name => "A Brand"),
:categories => [Category.new(:name => "A Category")]
}
@product = Product.new(default_attributes)
end
...
The Product Builder now contains an instance of a Product with default attributes. At this point the builder is able to return us a fully constructed Product provided we implement the following methods (one to return the Product in a persisted state, and the other to return the Product in a transient state):
...
def create_without_save
return @product
end
def create!
@product.save!
return @product
end
...
The create! method is useful if you are creating an integration test and don't want to manually save a Product each time. The create_without_save method is useful when writing unit/mock tests, where a database is not required (and would otherwise unnecessarily slow the test down). There are, of course, other scenarios where each of these methods will be useful.
Everyone has seen implementations of the Builder Pattern in various forms, one of the most ubiquitous being Java's StringBuilder/StringBuffer. However, whilst static languages such as Java must define each builder method at compile time, Ruby's method_missing allows us to implement a Builder pattern without defining a single builder method explicitly. Following is the method_missing implementation for the Product Builder:
...
def method_missing(method_name, *args, &block)
method_name = method_name.to_s.gsub("with_", "")
setter_method_name = method_name+ "="
if @product.respond_to?(setter_method_name)
@product.send(setter_method_name, *args)
end
return self
end
...
The method_missing is actually very simple. It extracts out the attribute name to set from the method name, meaning that a call to a method such as 'with_name("Blue Bike")' will be interpreted as setting the Product's 'name' to "Blue Bike".
A simple check as to whether the Product will accept the attribute is made and, if the product does support this attribute then the value is set in the Product.
Finally, note the ubiquitous 'return self' call at the end, allowing callers to build up multiple calls to the Product Builder on the same line.
So, what does the Product Builder look like in action? Let us assume that we are writing a test case where we want a set of Products with varying prices and brands, but we don't care about any other attribute. Here is how we may create 2 such products:
ProductBuilder.new.with_price(45.99).with_brand(Brand.new(:name -> "Sun")).create! ProductBuilder.new.with_price(50.01).with_brand(Brand.new(:name -> "IBM")).create!
If we cared only about the name and didn't want to have the products persisted:
ProductBuilder.new.with_name("Bobs Car").create_without_save
ProductBuilder.new.with_name("Jims Bike").create_without_save
Notice how the same Product Builder allows us to quickly and concisely create Product's with the attributes that we care about, whilst sensibly defaulting the attributes that we don't care about.
The Builder pattern is an especially effective tool in a developers repertoire when building complex data structures. A dynamic language such as Ruby makes the implementation of a Builder pattern even easier and more concise. The Builder pattern is especially effective when creating test data with large association trees, enable the test case to create very specific test data with a lot of flexibility and with minimal code.
It's an extremely effective pattern for creating test data, especially when coupled with avoiding the use of RoR's Test Fixtures (something I've covered in a previous blog).
Comments ...
read!! I definitely enjoying every little bit of it and I have you bookmarked
to check out new stuff you post.
basis.
post a link to this page on my blog. I am sure my visitors will find that very
useful.
the net and some are most defintely better than others. You have caught the
detail here just right which makes for a refreshing change – thanks.