17

I'm impressed at the brevity and usefulness of Cucumber's Scenarios, they're a great way to test a load of different cases.

e.g. example Cucumber scenario

Feature: Manage Users
In order to manage user details
As a security enthusiast
I want to edit user profiles only when authorized

Scenario Outline: Show or hide edit profile link
  Given the following user records
    | username | password | admin |
    | bob      | secret   | false |
    | admin    | secret   | true  |
  Given I am logged in as "<login>" with password "secret"
  When I visit profile for "<profile>"
  Then I should <action>

  Examples:
    | login | profile | action                 |
    | admin | bob     | see "Edit Profile"     |
    | bob   | bob     | see "Edit Profile"     |
    |       | bob     | not see "Edit Profile" |
    | bob   | admin   | not see "Edit Profile" |

(Code taken from Ryan Bates' More on Cucumber Screencast)

Is there an equivalent in RSpec?

I'd like to do the same thing in RSpec and to DRY up my code by reducing different tests down to a row in a scenario table.

Although I could write the code to do this myself, the fact that I'm considering it makes me wonder two things

  1. If this is useful it probably already exists in which case how do I use it?

  2. If it doesn't exist it suggests it shouldn't be done this way and that I'm approaching the problem incorrectly, how should I rethink my approach to RSpec?

Which answer is right and if it is useful, how do I do it?

Peter Nixey
  • 16,187
  • 14
  • 79
  • 133

4 Answers4

21

Try the following approach. I like the way it turned out.

describe StateDateMethods do
  before :each do
    @product = OpenStruct.new
    @product.extend StateDateMethods
  end

  def parse_date(unparsed_date_value)
    unless unparsed_date_value.nil?
      DateTime.strptime(unparsed_date_value, '%m/%d/%Y')
    end
  end

  context '#pre_order?' do
    examples = [
      # [visible_on, pre_order_on, for_sale_on] => method_result
      { :inputs => [nil, nil, nil], :expected => false },
      { :inputs => ['1/1/2001', nil, nil], :expected => false },
      { :inputs => ['1/1/2001', '1/1/2001', nil], :expected => true },
      { :inputs => ['1/1/2001', '1/2/2001', nil], :expected => true },
      { :inputs => ['1/1/2001', '1/1/2001', '1/2/2001'], :expected => false },
      { :inputs => ['1/1/2001', '1/1/2001', '1/1/3001'], :expected => true },
      { :inputs => ['1/1/2001', '1/1/3001', '1/2/3001'], :expected => false },
      { :inputs => ['1/1/3001', '1/1/3001', '1/2/3001'], :expected => false },
      { :inputs => ['1/1/2001', nil, '1/1/2001'], :expected => false },
      { :inputs => ['1/1/2001', nil, '1/1/3001'], :expected => false }
    ]
    examples.each do |example|
      inputs = example[:inputs]

      it "should return #{example[:expected].inspect} when visible_on == #{inputs[0].inspect}, pre_order_on == #{inputs[1].inspect}, for_sale_on == #{inputs[2].inspect}" do
        @product.visible_on = parse_date(inputs[0])
        @product.pre_order_on = parse_date(inputs[1])
        @product.for_sale_on = parse_date(inputs[2])

        @product.pre_order?.should == example[:expected]
      end
    end
  end
end

I think this gives the best of both worlds, because it keeps me from repeating myself, and it creates a different test for each condition.

Here's what a failure looks like:

....F.....

Failures:

  1) StateDateMethods#pre_order? should return false when visible_on == "1/1/2001", pre_order_on == "1/1/2001", for_sale_on == "1/2/2001"
     Failure/Error: @product.pre_order?.should == example[:expected]
       expected: false
            got: true (using ==)
     # ./spec_no_rails/state_date_methods_spec.rb:40:in `block (4 levels) in <top (required)>'

Finished in 0.38933 seconds
10 examples, 1 failure

Failed examples:

rspec ./spec_no_rails/state_date_methods_spec.rb:35 # StateDateMethods#pre_order? should return false when visible_on == "1/1/2001", pre_order_on == "1/1/2001", for_sale_on == "1/2/2001"

And here's what all green looks like:

..........

Finished in 0.3889 seconds
10 examples, 0 failures
M. Scott Ford
  • 2,859
  • 1
  • 23
  • 24
3

I give an example appropriate for RSpec in the question RSpec Scenario Outlines: Multiple Test Cases. I give one possible solution, but please let me know if you find a better one.

Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
2

I would not use RSpec in this way. RSpec should be used to drive behavior into the class one small behavior at a time. Since each behavior is unique you should use a different spec to define it.

In the Scenario above you might have specs that specify behavior like:

it "should allow user to edit his own profile"
it "should allow admin to edit other users profile"
it "should not allow non-admin to edit admin profile"
it "should not allow anonymous user to edit any profile"

One more thing, it is not a good idea to use RSpec to drive through multiple layers of your application. In other words, when you are defining your controllers you should mock the interactions with your models, etc.

Cheezy
  • 789
  • 3
  • 6
  • Hi cheezy, thanks for that. Although I was using an integration test example from cucumber I did know (although don't always manage to pull off) the granularity that should be used with RSpec. My question was less about that particular spec though and more about the idea of having table-driven scenarios. Do you know if RSpec supports that? – Peter Nixey Jan 17 '11 at 10:27
  • I have not see a gem that provides tables in rspec. Again, I cannot think of an example in which this would be appropriate. – Cheezy Jan 17 '11 at 12:00
  • 1
    Along the same lines, there is a gem that provides the Given/When/Then keywords in rspec that I have been playing with a little. https://github.com/jimweirich/rspec-given – Cheezy Jan 17 '11 at 12:04
  • what about the example I give in the question [RSpec Scenario Outlines: Multiple Test Cases](http://stackoverflow.com/questions/5524056/rspec-scenario-outlines-multiple-test-cases)? – ma11hew28 Apr 02 '11 at 15:46
  • 4
    Although RSpec is normally used at the unit-testing level (one class at a time), it can also be used for building integration tests which work at the same level as Cucumber. This can sometimes be an acceptable approach when there's no need to share acceptance criteria with non-technical stakeholders. – Andy Waite May 07 '11 at 11:08
  • I agree with @Cheezy in that the idea of RSpec is to be used to drive behavior. Also it makes it explicit and verbose as to what "case"/logic is being tested in each test, as opposed to just driving through a bunch of rows in a table. I like that format more in Cucumber/end to end testing/User scenarios. RSpec is better off used for unit testing, and in some special cases integrations testing.(Like the request specs motivations as mentioned by @"Andy Whaite". – jake Sep 27 '11 at 03:10
0

For table-driven / parameterized testing with RSpec there are now a couple of gems which might be helpful:

odlp
  • 4,984
  • 2
  • 34
  • 45