Search code examples
ruby-on-railsrubyrspeccucumberacceptance-testing

Mocking an expensive resource in acceptance tests (rspec, cucumber)


I'm in a bit of a quandary about how to go about writing a full stack acceptance test that involves on the lower levels interfacing with an expensive network call I'm trying to avoid.

I'm using rspec and cucumber, and doing most of my mocking with rspeck mocks.

The following is a contrived example of what I'm doing to illustrate the context of what I'm testing.

You have a page for adding ingredients with which you can later construct recipes and what-not. Upon the successful creation of a new ingredient, the ingredient model hits a web service to fetch usda nutritional information about the given food item and store it in the ingredient model.

I have written a WebServiceInterface class that is responsible for taking the name of a food item, making the appropriate request via the webservice and returning the xml response. Another helper class then parses the xml response and then returns a collection of the nutrition information that is then used in the ingredient instance.

Now, I have already written a suite of unit tests that excersize the WebServiceInterface. It looks something like this


fake-webservice = double('WebserviceInterface')
fake-webservice.should_receive(:get_response_xml).with(ingredient).and_return(fake_xml_response)

result = ResponseParser.new(fake-webservice).get_ingredient_nutritional_info
result.calories.should eql(123)
result.saturated_fat.should eql(10)

Note that get_response_xml is called internally by response parser. In this case ResponseParser calls the get_response_xml on the injected fake-webservice.

For models it works out just fine and dandy, but the situation is more complex in an acceptance test. I want to write an acceptance test for the page that tests the integration between the "Create new ingredients" page (new_ingredient_path) and the ingredient index path that lists all ingredients with their nutritional info (ingredients_path).

The acceptance test looks something like this:

visit new_ingredient_path
fill_in 'ingredient_name' :with => 'pickled herring'
click_button 'Create ingredient'
visit ingredients_path
page.should have_content 'Pickled Herring 123 calories 10g saturated fat'

Verbose context aside, this is the question: What is the best way to write this type of acceptance test while ensuring that actual web service is not called in the process.

The service charges per request and I test a lot so "You should really be testing all interaction in an integration/acceptance test" just doesn't suffice for me in this situation.

I need to find a way to ensure that a fake webservice or something like it is used instead of the actual webservice in the test environment (perhaps even development?). Coming from a .Net background, the typical answer I'm used to hearing is "Use Dependency Injection!!?!!". Although I think DI has its merits (please do not turn this into a DI debate), I just think there is a simpler lightweight solution here.

I have though of doing something like If ENV["RAILS_ENV"] == 'test' then use a fake, else use the real one. Is this a commonly used idiom in the rails environment or is it too tacky?

Is there some way to register this as some type of provider that can be configured in say a 'spec_helper.rb' block or a cucumber features/support/env.rb block?

Thoughts, ideas, input, advice???? Can any experience Rails dev. and or Cucumber/Rspec guru tell me what the common method of accomplishing this type of testing is?


Solution

  • With an acceptance test like Cucumber, I would prefer to actually exercise all parts of the system for real. Is there a test server for the webservice in question? Most good web-services should have one.

    Otherwise, you can use webmock to to stub out the webservice without modifying your code - since you seem to know the input and outputs of the webservice

    http://rubygems.org/gems/webmock

    The VCR gem - which uses webmock too - can be used to 'record' and save HTTP responses so that you only hit the webservice once ever

    https://www.relishapp.com/myronmarston/vcr