Specing Cucumber Step Definitions

4 Mar

Testing your tests is kind of crazy. However when writing a library of Cucumber step definitions which will be used in many projects it started to make sense to test my tests.

  • The step definitions are the code.
  • It helps reduce fear of breaking lots of projects which use the steps.
  • The tests/specs show examples of how to use the step definitions.

It is important to note that I’m not imply TDD/BDDing these step definitions. My use-case is adding tests afterwards when it comes time to extract them to a library.

How to test step definitions

Exercise the full step (with Rspec)

The common way of testing complex steps is to extract all the ruby from the step definitions and then just test that. But this way of testing does not exercise the step definitions from the outside, getting as close as possible to how they will be used. It also does not provide examples of how to use the step definitions.

So I sat down with Matt Wynne, who started this discussion and we thrashed out some Rspec macros for testing whole step definitions.

If we were testing this step definition (icalendar_steps.rb):

require 'icalendar'

module Cucumber
  module Stepdefs
    module Icalendar
      def response_calendars
        ::Icalendar.parse(response.body)
      end

      def response_events
        response_calendars.length.should == 1
        response_calendars.first.events
      end
    end
  end
end

Before('@ical') do
  extend Cucumber::Stepdefs::Icalendar
end

Then /^the iCalendar should have exactly (\d+) events?$/ do |number_of_events|
  response_events.length.should == number_of_events.to_i
end

Our spec would look like this (Note: we test the Before hook as well as the step definition):

describe 'icalendar_steps' do
  step_file File.dirname(__FILE__) + '/../../../lib/cucumber/stepdefs/icalendar_steps'

  # Test that the Before hook is not called when there is no tag
  without_tags do
    it "should not mix in any calendar related methods" do
      world_methods.should_not include('response_calendars')
      world_methods.should_not include('response_events')
    end
  end

  # Test the Before hook mixes in the right methods when tagged with @ical
  with_tag '@ical' do
    ['response_calendars', 'response_events'].each do |method|
      it "should add the #{method} to world" do
        world_methods.should include(method)
      end
    end

    the_step "the iCalendar should have exactly 1 event" do
      describe "when 1 calendar with 0 events is in the response body" do
        before(:each) do
          world.stub!(:response).and_return(mock_response_with_0_events)
        end

        it_should_fail_with(Spec::Expectations::ExpectationNotMetError)
      end

      describe "when 1 calendar with 1 event is in the response body" do
        before(:each) do
          world.stub!(:response).and_return(mock_response_with_1_event)
        end

        it_should_pass
      end
    end
  end
end

Experiment’s Source code

You can see the source on Github:

git clone git://github.com/mattwynne/cucumber-step_definitions.git

If this experiment proves successful these macros will make their way to a nice gem.

  • Hi Matt, thanks for the interesting comment, parenthesis and all.

    The idea with these specs is that they are testing a set of cucumber step definitions that are separated from their usage, usually deployed as a gem. Hence the idea is the tests are for the maintainers/contributors and to provide examples for people looking to use the steps (The specs are the documentation for the steps). So there are multiple sets of tests to examine in the same way gems used in your application might might have their own tests.

    Complex steps can certainly be refactored and I would encourage that. In this case its not so much about the complexity of the step, its about cutting through the whole step. Also that complexity in the regular expression is not so easy to factor out. For example: if you mess up the regexp and you are only testing the ruby then no tests fail and you push a new version of the gem that could break the other libraries using it.

    Regarding CI, these are the only tests for this library. Updates to these steps follows much the same workflow as a gem

    So far I've decided not to mess with the describe method as I think as a core method of Rspec it has an expected behaviour. I also like the custom dsl path as it allows methods that better reveal my intent and helps make the tests easier to read.
  • Matthew Bennett
    *disappointed look* Joe Joe Joe Joe Joe!!! Clearly you're going mad without my "sanity" to keep you going. Where will it end?

    Think about the idea "it_should_pass". Well, either it does or it doesn't. Imagine these specs pass, you think your code does what it's meant to do, but your cucumber test fails. Who do you blame? You've gone from having two possible culprits, and now you're trying to work out which of the three is wrong.

    Don't get me wrong - never say never. But I would definitely say rarely. VERY rarely. Maybe if your step definitions are particularly complex (so why haven't you refactored it?) or in something like Clearance where you have generators in your gem that populate features and steps for you (in which case, surely a test project is easier to maintain? Using real example code to test your tests, rather than increasingly abstract levels of test.) Of course, you've said when you come to extract tests for reuse (but then why not make the original or simplest project the benchmark?)

    As a point of interest, which comes first in your CI - these (giving you that little extra confidence that your tests are working at the expense of speed-to-feature?) or your actual tests (thereby speeding up your tests, pushing the functionality to the top that you actually care about, and risking that people abort the tests before the test's tests even run?) Or do you only ever run these tests in isolation from the projects that actually use them (running the risk of these tests rotting away because the steps are updated and not pushed upstream?)

    On a positive note, nice dsl - were you tempted to override "describe" to test whether the text matches a step, or would that have been too magicky? Certainly a very interesting project, if bordering on the insane - will try it, just for the fun of it! I'm sure I can find a complicated enough project ;)

    (This comment was sponsored by the humble parenthesis...)
blog comments powered by Disqus