Posted by aaron at 09:01AM, Thursday, December 03rd, 2009
Testing and mocks using Rails Plugins
A while ago, I embarked on a work project that entailed refactoring a Rails application into a plugin, containing most of the core functionality, and a set of applications that used that plugin. Given how easy Rails makes a lot of test-driven development, I was somewhat surprised to find a lack of discussion or documentation about best practices for pain-free testing of both the core plugin and the applications -- so I figured it out myself.
The problem that I was trying to fix was that I needed to have test coverage of methods in the models and controllers in the plugin, but I also wanted to be able to test A. that functionality that had test coverage in the plugin would also get the same test coverage in the app, where that was appropriate, and B. any functionality I changed or overrode in the applications. I did *not* want to force developers to copy and paste big blocks of code from my plugin into all their applications every time I made a change in the plugin. Complicating this, I had used mock objects extensively, to allow changing the functionality of code under test so that, for instance, I could access private methods from test code, or so I could force certain conditions to occur for the test. And I didn't want developers to have to copy and paste the mock definitions from the plugin to the applications, either.
Here's what I ended up doing:
For application code:
For any class that I redefined in an application, I simply required the version defined in the plugin at the top of the app's class definition. (This seems to be necessary because of the default class load order when using Rails Plugins.) Every time I used a require statement anywhere in code, I used
File.expand_path to fully qualify paths, rather than using relative paths; this ensured that the same file would never get loaded twice, when it might be required by files in different directories.
For test code:
For mocks in the plugin:
In the mocks in the plugin, I required (using
File.expand_path) the original class definitions that I was redefining, at the top of the class.
This require was necessary to facilitate the correct load order when the mock object was loaded in tests in the applications using the plugin.
For mocks in applications:
For mocks defined in the applications, I decided that the correct load order was:
- 1. The plugin's initial definition of the class
- 2. The plugin's re-definition of the class in its mock object
- 3. The app's (re-)definition of the class.
There are situations in which I might need to re-define a method in both places, but it seemed safer this way, since anything I'd have to redefine in the app's mock was probably either a method unique to the application (and not in the plugin), or a method that I'd already redefined in the application, and so probably needed a new mock definition anyway. So in each application mock, I explicitly required, first, the plugin's mock definition, then the app's (not-mock) definition of the class.
In the plugin and application test helpers, since I had environment-specific code, and never wanted to load both the app's test helper and the plugin's, for any application, I set a
LOADED_TEST_HELPER value, unconditionally, in both test_helpers. I also stuck just the method definitions for the plugin's test helper in a module which I required from both of the test helpers.
Tests in the plugin:
In tests in the plugin, I required the plugin's test_helper, if
LOADED_TEST_HELPER hadn't yet been set. All the mock objects got implicitly loaded, since the mock path shows up early in the
load_paths definition in the test environment. The mocks, in turn, required the plugin's definition of their respective classes. Hunky dory.
Tests in applications:
The desired load order here is:
- 1. Plugin's definition of the class
- 2. Plugin's re-definition of the class in its mock object
- 3. App's re-definition of the class
- 4. App's re-definition of the class in its mock object
- 5. Plugin's definition of the test class
- 6. App's re-definition of the test class
To accomplish this, in tests in the app, I required the *app's* test helper. Then I required the test defined in the plugin. (I sometimes did this, for instance, in functional tests against controllers that were unchanged from the plugin to the app, just as a sanity check that no environment settings borked anything: require the helper and the plugin's test, and then don't define any methods. In other application tests, of course, I'd have test method declarations for application-specific functionality.) The resulting load order for tests in the applications is therefore:
- app test, which would require:
- app test helper, which would load the app's mocks directory, and load:
- app mock, which would first require:
- plugin mock, which would first require:
- plugin class definition, with 1. all its method declarations
- then 2. (re)define its own methods
- then require:
- app class definition, which would in turn require:
- the already-loaded plugin class definition; because I used expand_path, this require had no effect in this context
- then 3. (re)define its own methods
- then 4. (re)define its own methods.
- then require:
- plugin's test, which would check for
LOADED_TEST_HELPER, which was already set, then 5. define the base test class methods
- then 6. (re)define its own test methods.
It ends up being relatively trivial to use the necessary require statements in various class definitions, and after a couple of months, the most difficult thing to coordinate between changes to the plugin and changes to the app has ended up being the test fixtures!
All content copyright © 2001-2009 the owners of http://www.circa75.com/