circa75 Home | About circa75 | Articles | Links | Contact Us

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:


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.

Test helpers:


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:


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:


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!

circa75 Home | About circa75 | Articles | Links | Contact Us

All content copyright © 2001-2009 the owners of http://www.circa75.com/