Flexible Fixtures in Rails Unit Tests
Posted by Christopher Wojno Thu, 15 May 2008 19:25:00 GMT
I’ve found that when testing model classes, it’s important to have a database that is setup with records such that something fails or is configured to produces results in a particular way. With the current rails Fixture framework, this is impossible. Let me explain what I’ve been trying to do to give you a better idea of what is lacking in Rails:
Example Problem Description Finding Something That’s “Missing”
Suppose you have a database with a table called “cookies” and another table “users” and another table called “user_cookies.” As you can see, you have a set of users and types of cookies. Maybe the cookies are chocolate or mint, etc. However, these are cookies of the month. Each month, a new cookie is released. To keep track of which month the cookie is for, we include a year and month field in the cookie table. Now, if we have users that want to track which cookies they’ve tried over the course of the years, we need to either keep a list of all the cookies they’ve eaten, or search for holes in the cookie order.
Even if we had a list, say we had a weekly and monthly cookie. If we wanted to know which monthly cookies we’re missing, the list will be of no use as well, as the list cannot differentiate between monthly or weekly. We’re forced to look for holes. Luckily, such a request is rare in our system, so a brute force search will be small (there are ways of making it faster, but that’s beyond the scope of this article).
Rails Testing Framework “Deficiency”
Don’t get me wrong, the testing framework for Rails is well done, easy to use, and mostly comprehensive. However, the Fixture support is a bit lacking. If you want to have multiple types of tests that act on the database records as a whole, you need custom fixtures. For the above problem, we need a fixture to test cookie lookups, but we also need a test to detect missing cookies.
So we need multiple fixtures we can load with different tests. Well, that’s not really possible with the standard testing framework. The class method “fixtures” for Test::Unit::TestCase (the testing base class) will only load up the standard fixtures. Bummer. I want to create subdirectories in the fixtures directory that are named after the model being tested. Within those directories, there will be more directories that name the test in progress. Within THAT directory will be the .yml files for custom database setups.
So the directory structure is of this form:
|-+BASE RAILS APPLICATION | |-+ test | | |-+ fixtures | | | |-+ MODEL_NAMEs | | | | |-+ test_find_missing_cookies | | | | | |- users.yml | | | | | |- cookies.yml | | | | | |- user_cookies.yml | | | | |-+ test_find_favorite_cookie | | | | | |- user_cookies.yml ...
Not only can you deploy custom fixtures for models and more than just one, you can also specify tables to populate other than the model with which you’re working. This provides as much flexibility as I can fathom that I need.
But, alas, you need a way to load these fixtures up. Admittedly, my method is a bit messy and slow if you use the traditional Fixtures. You need to modify the test_helper.rb file. Add this to the Test::Unit::TestCase class:
@@model_name = nil
def self.fixture_model( m )
@@model_name = m
end
protected
def load_subfixtures( test_method, pfixture_table_names = [], pfixture_class_names = [] )
raise StandardError.new( 'load_subfixtures requires that you specify the model name in the class by calling the class method: fixture_model' ) if @@model_name.nil?
fixture_path = File.dirname(__FILE__) + '/fixtures/'+@@model_name.to_s+'/'+test_method+'/'
return super unless File.exists?( fixture_path )
oldpath = fixture_path
oldtables = fixture_table_names
oldclasses = fixture_class_names
@@fixture_path = fixture_path
@@fixture_table_names = pfixture_table_names
if @@fixture_table_names.empty?
@@fixture_table_names = fixture_table_names
else
@@fixture_table_names.push @@model_name
end
@@fixture_class_names = pfixture_class_names
@@fixture_class_names = fixture_class_names if @@fixture_class_names.empty?
oldret = load_fixtures
@@fixture_path = oldpath
@@fixture_table_names = oldtables
@@fixture_class_names = oldclasses
return oldret
end
As you can see, it does not override the fixture initializer for the unit tests. It merely calls it again. Here’s how you use it:
require File.dirname(__FILE__) + '/../test_helper'
class UserCookieTest < Test::Unit::TestCase
fixture_model :user_cookies
# Replace this with your real tests.
def test_find_missing_cookies
load_subfixtures( 'test_find_missing_cookies', [:users,:cookies] )
assert_equal 2, UserCookie.count
end
end
For the test: test_find_missing_cookies, it will load up the .yml files: user_cookies.yml, cookies.yml, and users.yml for use with that test.
You need to specify the fixture_model as this is the directory it will look in for additional sub-fixtures. If you don’t the call will fail with an exception.
Problems
Obviously, it’s not very smart. It can’t automatically detect the test name. Though, this allows you to re-use fixtures from other tests as well as mix and match the sub-fixtures loaded with the test. But it requires that you manually type in the test name for every test.
In addition, if the fixture has already been loaded using the standard class method call: fixtures, it will be loaded and overwritten by the test a second time. This will slow the testing process down.
I’m happy with it. I bet you’ve been waiting for something like this too. Enjoy!
Edit
An update to this article is posted here.
