Ok, so I've been using
shoulda for the last couple of weeks, and while its certainly reduced the
amount of duplicated code in my test, and given me more meaningful test descriptions, I've also noticed that its a little hard to find things with all the indented levels of contexts, setups, and
shouldas. And I find that I still end up with duplicated setup and testing code. Fortunately, this is fairly easy to address by writing your own shoulds and context methods.
Custom shoulda's
Lets look first at writing your own
shoulda method. Lets say you are testing a method that parses some file and returns the extracted data, along with a lit of any errors it found:
result, errors = MyClass.parse(file)
We want to test this with a number of different test files, so we end up with tests looking like this:
context "when parsing abc.tst" do
setup do
@result, @errors = MyClass.parse("abc.tst")
end
should "have no errors" do
assert @errors.empty?
end
should "have some expected result" do
....
end
end
So a number of our tests is going to be checking that @errors is empty. While its only a few lines, it does add some
visual clutter, and is easy to clean up with our own
shoulda method:
def self.should_have_no_errors
should "have no errors" do
assert @errors.empty?
end
end
This needs to be before your tests are defined, since it is a class method which will be executed during the loading of the class. Now, we can replace three lines with one:
context "when parsing abc.tst" do
setup do
@result, @errors = MyClass.parse("abc.tst")
end
should_have_no_errors
should "have some expected result" do
....
end
end
Our
shoulda methods can also take parameters, so if we wanted to test for the presence of errors, we could add a should_have_error method:
def self.should_have_error(regex)
should "have error matching #{regex.to_s}" do
assert_match regex, @errors.first
end
end
which we would use with
should_have_error /Invalid Attribute/
Custom Contexts
The next thing we notice is that a lot of our tests are going to have similar setup sections, but we can't use a common context because the file names are different. To address this, we can create a custom context:
def self.when_parsing(filename, &block)
context "when parsing #{filename}" do
setup do
@result, @errors = MyClass.parse(filename)
end
block.bind(self).call
end
Now, our tests look like this:
when_parsing("abc.tst") do
should_have_no_errors
should "have some expected result" do
...
end
end
I find this
particularly convenient for handling
logins when testing rails
functional tests
:
def self.logged_in_as(sym, &block)
context "logged in as #{sym.to_s}" do
login_as(sym)
end
block.bind(self).call
end
logged_in_as(:quentin) do
[test normal user stuff]
end
logged_in_as(:admin) do
[test admin access]
end