testing a multi-step workflow in rspec
I'd like to know about idioms or best practices for testing a multi-step workflow using rspec.
Let's take as an example a "shopping cart" system, where the buying process might be
- when user submits to basket and we are not using https, redirect to https
- when user submits to basket and we are using https and there is no cookie, create and display a new basket and send back a cookie
- when user submits to basket and we are using https and there is a valid cookie and the new item is for a different product than the first item, add a line to the basket and display both lines
- when user submits to basket and we are using https and there is a valid cookie and the new item is for the same product as a previous one, increment that basket line's quantity and display both lines
- when user clicks 'checkout' on the basket page and is using https and there is a cookie and the basket is non-empty and ...
- ...
I've read http://eggsonbread.com/2010/03/28/my-rspec-best-practices-and-tips/ which advises i.a that each "it block" should contain only one assertion: instead of doing the computation and then testing several attributes in the same block, use a "before" inside a context to create (or retrieve) the object under test and assign it to @some_instance_variable, then write each attribute test as a separate block. That helps a little, but in a case such 开发者_运维问答as outlined above where testing step n requires doing all the setup for steps [1..n-1] I find myself either duplicating setup code (obviously not good) or creating lots of helper functions with increasingly unwieldy names (def create_basket_with_three_lines_and_two_products) and calling them consecutively in each step's before block.
Any tips on how to do this less verbosely/tediously? I appreciate the general principle behind the idea that each example should not depend on state left behind by previous examples, but when you're testing a multi-step process and things can go wrong at any step, setting up the context for each step is inevitably going to require rerunning all the setup for the previous n steps, so ...
Here's one possible approach -- define an object that creates the necessary state for each step and pass it forward for each successive one. Basically you need to mock/stub the method calls for all the setup conditions:
class MultiStep
def initialize(context)
@context = context
end
def init_vars
@cut = @context.instance_variable_get(:@cut)
end
def setup(step)
init_vars
method(step).call
end
def step1
@cut.stub(:foo).and_return("bar")
end
def step2
step1
@cut.stub(:foo_bar).and_return("baz_baz")
end
end
class Cut # Class Under Test
def foo
"foo"
end
def foo_bar
"foo_bar"
end
end
describe "multiple steps" do
before(:each) do
@multi_stepper = MultiStep.new(self)
@cut = Cut.new
end
it "should setup step1" do
@multi_stepper.setup(:step1)
@cut.foo.should == "bar"
@cut.foo_bar.should == "foo_bar"
end
it "should setup step2" do
@multi_stepper.setup(:step2)
@cut.foo.should == "bar"
@cut.foo_bar.should == "baz_baz"
end
end
Certainly too late for OP, but this could be handy for others - the rspec-steps gem seems to be built for this exact situation: https://github.com/LRDesign/rspec-steps
It might be worthwhile to look at https://github.com/railsware/rspec-example_steps and https://github.com/jimweirich/rspec-given as well. I settled on rspec-steps, but I was in a rush and these other options might actually be better for all I know.
精彩评论