Building a plugin on RSpec
Yesterday (Nov 16), on the Rails Mailing List, Luke Redpath expressed some concerns over Rails handling of tests. That thread references Dave Astels’ RSpec.
I read Dave’s A New Look at Test Driven Development (PDF, 136 Kb) in which he advocates switching to Behavioral Driven Development. It was so interesting that I just had to start playing around with it.
I have been thinking about writing a Rails plugin to manage has_and_belongs_to_many associations, and this just felt like the right-sized project to try RSpec with.
Has and belongs to many associations are great: they allow you to join two tables in a M:N relationship. Most of the time, the relationships are managed using two lists side by side, with some kind of user interface element to transfer items from the available to the selected list, and vice-versa. Rails has no built-in helpers to manage them.
A quick description of how the plugin works, and then on to how I wrote it using RSpec: an HTTP request comes in, we inspect the params Hash to convert anything that’s a list of IDs into an Array of IDs (to pass to #collection_singular_ids). On the other side, we have a helper that generates two select elements, with anchor elements as the user interface to move items from one list to the other.
Since this article is about writing specifications rather than on how to write a Rails plugin, let’s start specifying !
Installation and setup
I started by installing the gem. Then, I created a Rails-like hierarchy:lib/, test/ and vendor/. I then unpacked the gem into vendor:
That gave me the library’s full source code right next to mine, for easy inspection and analysis.
Next, I needed a way to run my expectations (we’d usually call these tests, but since we’re trying to move away from the testing terminology…). I wrote the following Rakefile:First behavioral expectation
The first expectation I wanted to write was to convert an emptyString into an empty Array. That lets us handle the case where no items are selected. I chose to write this expectation first because we’re going to need it anyway, and it was a good introduction to the RSpec framework for me.
We start by requiring spec, and then the filter’s code. Next, we create a new class that describes the context of this particular set of expectations. This is the expectation’s fixture, so to speak. Notice we extend Spec::Context (short for Specification Context) instead of Test::Unit::TestCase. Then, we make our lives easier by including the code under scrutiny in the context.
Then, we have the setup method. Notice I am executing the filter directly in the setup method. This is the most striking difference I found between Test::Unit and RSpec. All behavioral specifications I wrote to date have used this pattern.
The last item in the class is the actual specification. We are asserting that when “No selected entries” we “should replace string with empty array”.
Let’s write the minimum amount of code, just to get started: Let’s see that in action:(I removed the Rake backtrace from this and all other example runs in the interest of shortening an already long article)
What is this telling us ? It’s telling us that an empty string should have been equal to an empty array. This is a direct translation of the specification we wrote.
Naming conventions
Reading further though, we see thatSpec::TextRunner has found *three expectations to run, not just one. Which ones are they ? If we look at the Spec::Context code, we’ll find this:
Ah ! There are no naming convention as in the xUnit case, so all methods are expectations, except the ones coming from our superclasses. That only means we have to move our code elsewhere. After refactoring, we now have:
Running that, we get:
Beautiful ! Only one specification was executed – let’s make it pass:
Running:
Even more beautiful: green bar !
More expectations
Let’s implement a second behavioral specification, but we’ll need a new context – we need to have something for the filter to work against. This means we have to create a new context for the expectations to execute in:
The setup method is very similar – in fact, it’s nearly identical, the only difference being the value we assign to the params Hash. Probably something we can extract superclass from. Let’s wait for the fabled three strikes and you refactor before we do so, though.
Excellent, another green bar. I will leave it as an exercise to the reader to write additional specifications, or else visit the habtm_helper Rails plugin and acquire the plugin through regular means.
Parting thoughts
I liked my first exposure to behavioral specifications. Of course, anything I had already learned for doing Test Driven Development I could immediately apply to Behavior Driven Development. This is mostly a semantical change, but it does have an impact on how one thinks.
I also liked the documentational aspect of behavioral expectations. Of course, the same thing is possible using testing code – we simply have write appropriate test case names and tests, and with a bit of regular expression magic, parse away the unnecessary bits. However, using RSpec, it felt more natural to write longer, more descriptive, expectation names.
After having coded and refactored some more, I have the following specifications for the filter:- When No Selected Entries
-
- should replace string with empty array
- When One Selected Entry
-
- should replace string with single element array
- should have entry id in list
- When Two Selected Entries
-
- should replace string with two elements array
- should include first selected id
- should include second selected id
- When Two Selected Entries With Whitespace
-
- should replace string with two elements array
- should include first selected id sans whitespace
- should include second selected id sans whitespace
- When Extra Entries In Params
-
- should leave non matched suffixes alone
- should process matching entries as usual
- When One Entry With Non Standard Name
-
- should replace string with single element array
- should have entry id in list
- When Processable Entry In Params Root
-
- should be processed like deeper params
What have we learned here today ?
RSpec is a different framework. It just takes a bit of time getting used to. Besides, the version describe here is 0.2.0, hardly enough to say it’s finished growing yet. On the other hand, I feel this is easier to work with than Test::Unit, and I will start pushing to get this added to Railsties. It would make a nice addition to the already existing testing framework.
setupis used just like in the testing world, except I often sawsetupcalling the code we are asserting against;- Don’t put your helper methods directly in the context class;
- You’ll probably call the code under specification in your
setupmethod; - Anything you learned while doing test driven development is applicable to behavior driven development.
Until later !
October 12th, 2007 at 10:25 PM
Any chance of packaging this up? I’d really love to use RSpec instead of Test::Unit for all my rails testing but the rails test_help stuff only works for Test::Unit.
Cheers,
Simon