Jason Earl
Personal Website
BLOG
Overlooked Rails Best Practises Pt.1
Why Yet Another Post?
I know so many people go on about Rails based coding practises. However, it seems despite all this harping that other people do, there are a large numbers that are out of tune with this. It’s some what has got to me as I really hate getting involved in projects when there are no clear guidelines on coding style or practises, because as soon as several programmers with varying styles get involved things get unconformable for everyone.
The concept of ‘Best Practises’ is certainly a very subjective issue. Certainly I will agree when it comes to basic code layout, there is no right or wrong way, likewise some of the points I will put here are going to be subjective, but this is what I think are some good starting points.
In this post, and probably some subsequent posts, I want to cover some basic principles that I personally think should be applied to all Rails projects as a form of defensive programming.
Fixtures Considered Harmful
I know some will think this is somewhat odd, but I hate fixtures with a passion. My problem with fixtures is that they generally tend to work on a global basis, and thus can manifest issues that are typical of programs that misuse global variables and other forms of undesired side effect created by a shared state.
The Shared State Scenario
A typical example that illustrates this is that you need to add or remove data to a set of fixtures in order to reflect a specific new feature you’ve just added. Because of the global nature of fixtures, many other tests that use that set of fixtures are at risk if breaking because you are sharing that data. These other tests are completely unrelated to changes you’ve made, and therefore are ‘leaky’ in nature because they shouldn’t be failing as they are totally unrelated.
Inefficient
Another issue I have with fixtures is that they are not expressive. They seems to carry a lot of baggage that is not needed. For instance if I’m writing a test that relates to the billing of a property, I only am wanting to focus on fields such the price of the property and maybe some date specific stuff if the system is subscription based. Yet with fixtures you end up carrying a whole load of extra baggage for all the other fields.
What would be far better is to have some sort of base shared state, which basically is just used to set up the bare minimum defaults required for the given model. This would mainly be there to ensure things such as validations are kept happy. Each test can then then simply modify / update attributes to this base data. Personally, I think this is far more expressive. For example:
A Possible Alternative To Fixtures
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# In your test helper or some other common/shared test code file you would define some example # model instances which can be used to encapsulate any necessary data that by design probably # needs to be shared. class ActiveRecord::Base def self.example(specifics = {}) create! self::EXAMPLE_DEFAULTS.dup.update(specifics) end end # Each model would need to define some EXAMPLE_DEFAULTS. # You might want to DRY things up by placing this in a YAML file, while this is similar to a # fixture in the sense that is shared data, this is minimal data that is simply used to ensure # the model just 'behaves' (e.g. saves without validation issues, etc). However the difference # here is are not defining large number of instances in your DB, of which each test is probably # only going to use a handful of these instances class Property EXAMPLE_DEFAULTS = { :summary => 'My test property', :description => 'Please buy me!', :price => 450_000.usd, :market => 'Sale', :classification => 'Residential', } end class User EXAMPLE_DEFAULTS = { :login => 'jase', :password => 'dont ask me', :credit_balance => 0.usd, } end # ... etc for each model # Now in your test cases you generate data on the fly rather than pull data from a fixture. # This way each test is only having to deal with data specific to the it's test case. describe Property, 'billing' do attr_reader :property, :user before :each do # Note how we create each instance we need, overriding field relevant to what our # test is going to cover @user = User.example :credit_balance => 50.usd @property = Property.example end it "should cost $10 to list a property" do property.active?.should be_false property.activate_listing! property.active?.should be_true user.credit_balance.should == 40.usd end # Put other related scenario examples here obviously end |
Using the technique illustrated above we manage to DRY up our test data in a similar way that fixtures try to keep things DRY up common data between tests. This also gets round the issues highlighted in the ‘shared state scenario’. That is if we decide to rename/delete a field such as User#credit_balance
, we only need to change code in test that use this field. In the case of modifying a field like Property#summary
, which is shared (because it is required through the use of a validation) everywhere, we only need to change it in one place, thus keeping our code DRY, and avoiding the ‘leaky failing case’ issue.
Improving Code Coverage
Not only does the above concept DRY up code and make it more robust, but we also improve our code coverage (If you haven’t heard of it, look at RCov). This is because we are using code to create our models code such as callbacks (i.e. before_create, after_create, etc) and validations will get called during our tests. Thus this helps reduce the flakiness of tests with very little extra effort.
Closing Words
Hopefully, I’ve illustrated the case why fixtures create brittle tests, and looked at a possible alternative style that addresses the major pitfalls incurred by using fixtures.
I’ll be planning to create a whole series of posts on this topic, so stay tuned.
Listening to Shoegaze:
Local Boy Makes God
- Amusement Parks on Fire