--- title: "Building a plugin on RSpec" created_at: 2005-11-17 18:00:00 blog_post: true tags: - plugins - spec-testing id: 19 filter: - erb - textile ---
"Has and belongs to many":http://api.rubyonrails.com/classes/ActiveRecord/Associations/ClassMethods.html#M000467 associations are great: they allow you to join two tables in a "M:N relationship":http://publib.boulder.ibm.com/infocenter/ids9help/topic/com.ibm.ddi.doc/ddi50.htm. 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 !
h2. 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:
<% code(:lang => "shell") do -%>$ cd vendor
$ gem unpack rspec
Unpacked gem: 'rspec-0.2.0'
<% end -%>
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:
<% code("Rakefile") do -%>require 'rake'
task :default => [:test_behaviors]
desc "Updates the load path to include all available vendor libraries as well as the code under test"
task :set_load_path do
Dir['vendor/*'].each do |dir|
next unless File.directory?(dir)
lib_folder = File.join(dir, 'lib')
next unless File.directory?(lib_folder)
$LOAD_PATH.unshift lib_folder
end
$LOAD_PATH.unshift 'lib'
$LOAD_PATH.unshift 'test'
Dir['test/**/*.rb'].each {|file| require file }
end
desc "Runs behavioral tests"
task :test_behaviors => [:set_load_path] do
require 'spec'
require 'spec/text_runner'
Spec::TextRunner.new.run
end
<% end -%>
h2. First behavioral expectation
The first expectation I wanted to write was to convert an empty String 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.
<% code("test/specs/no_selected_entries.rb") do -%>require 'spec'
require 'francois_beausoleil/habtm_helper_plugin/filter'
class NoSelectedEntries < Spec::Context
include FrancoisBeausoleil::HabtmHelperPlugin::Filter
attr_reader :params
def setup
@params = {:contact => {:group_ids => ''}}
habtm_filter
end
def should_replace_string_with_empty_array
params[:contact][:group_ids].should_equal []
end
end
<% end -%>
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:
<% code("lib/francois_beausoleil/habtm_helper_plugin/filter.rb") do -%>module FrancoisBeausoleil
module HabtmHelperPlugin
module Filter
def habtm_filter
end
end
end
end
<% end -%>
Let's see that in action:
<% code(:lang => "shell") do -%>$ rake
(in D:/habtm)
.X.
1)
<"":String>
should be equal to:
<[]:Array> (Spec::Exceptions::ExpectationNotMetError)
./test/specs/no_selected_entries.rb:14:in `should_replace_string_with_empty_array'
Finished in 0.016 seconds
3 specifications, 3 expectations, 1 failures
<% end -%>
_(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.
h3. Naming conventions
Reading further though, we see that Spec::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:
<% code("vendor/rspec-0.2.0/lib/spec/context.rb") do -%>module Spec
class Context
private
def self.my_methods
self.instance_methods - self.superclass.instance_methods
end
def self.specifications
return self.my_methods.select {|spec| self.specification_name?(spec)}
end
def self.specification_name?(name)
return false unless self.new.method(name).arity == 0
return false if name[0..0] == '_'
true
end
end
end
<% end -%>
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:
<% code("test/spec_helper.rb") do -%>require 'spec'
require 'francois_beausoleil/habtm_helper_plugin/filter'
class DummyController
attr_accessor :params
include FrancoisBeausoleil::HabtmHelperPlugin::Filter
end
<% end -%>
<% code("test/specs/no_selected_entries.rb") do -%>require 'spec_helper'
class NoSelectedEntries < Spec::Context
def setup
@controller = DummyController.new
@controller.params = {:contact => {:group_ids => ''}}
@controller.habtm_filter
end
def should_replace_string_with_empty_array
@controller.params[:contact][:group_ids].should_equal []
end
end
<% end -%>
Running that, we get:
<% code(:lang => "shell") do -%>$ rake
(in D:/habtm)
X
1)
<"":String>
should be equal to:
<[]:Array> (Spec::Exceptions::ExpectationNotMetError)
./test/specs/no_selected_entries.rb:11:in `should_replace_string_with_empty_array'
Finished in 0.015 seconds
1 specifications, 1 expectations, 1 failures
<% end -%>
Beautiful ! Only one specification was executed - let's make it pass:
<% code("lib/francois_beausoleil/habtm_helper_plugin/filter.rb") do -%>module FrancoisBeausoleil
module HabtmHelperPlugin
module Filter
def habtm_filter
params[:contact][:group_ids] = []
end
end
end
end
<% end -%>
Running:
<% code(:lang => "shell") do -%>
$ rake
(in D:/habtm)
.
Finished in 0.0 seconds
1 specifications, 1 expectations, 0 failures
<% end -%>
Even more beautiful: green bar !
h2. 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:
<% code("test/specs/one_selected_entry.rb") do -%>require 'spec_helper'
class OneSelectedEntry < Spec::Context
def setup
@controller = DummyController.new
@controller.params = {:contact => {:group_ids => '624'}}
@controller.habtm_filter
end
def should_have_one_element_in_the_array
@controller.params[:contact][:group_ids].size.should_equal 1
end
def should_have_the_id_as_the_first_element
@controller.params[:contact][:group_ids][0].should_equal '624'
end
end
<% end -%>
Hash. Probably something we can "extract superclass":http://www.refactoring.com/catalog/extractSuperclass.html from. Let's wait for the fabled "three strikes and you refactor":http://c2.com/cgi/wiki?ThreeStrikesAndYouRefactor before we do so, though.
Next, we get our behavioral specifications. The first one says when we have "One selected entry" we "should have one element in the array". This is all very readable and reasonable. Then, when we have "One selected entry" we "should have the id as the first element". Let's see this in action:
<% code(:lang => "shell") do -%>$ rake
(in D:/habtm)
XX.
1)
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.
Some things to remember:
* setup is used just like in the testing world, except I often saw setup calling 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 setup method;
* Anything you learned while doing test driven development is applicable to behavior driven development.
Until later !