Mocking Controller Method Responses in Capybara Tests: Advanced Tips

Written by

When writing tests in Capybara, you often want to isolate specific portions of a class. You might want to create a unit test, or perhaps you just don’t have the required connection access to pull data required by a class method.

Within a Capybara test, it is possible to mock the response of a controller method before it is instantiated. So, for example, you can have this Ruby class:

class Employee
 include AssetsUrl
 
 attr_accessor :name, :email, :picture_id, :proficiencies
  def initialize(params = {})
   @name = params.fetch(:name, '')
   @email = params.fetch(:email, '')
   @picture_id = params.fetch(:picture_id, '')
   @proficiencies = params.fetch(:proficiencies, {})
 end
  def picture_url
   AssetsUrl.get_main_url + '?img=' + @picture_id
 end
 
 def get_proficiency(proficiency)
   @proficiencies[proficiency] || 'none'
 end
 
 def set_proficiency(proficiency, value)
   @proficiencies[proficiency] = value
 end
end

Notice how picture_url depends on an external class AssetsUrl to get the main part of an image URL (perhaps all of your employee profile pictures reside in the same external server); but that class might not be available in your unit test scope (or available within your UI test permissions), so you might want to mock that response in order to proceed with your tests.

You can mock any calls to picture_url completely by using allow_any_instance_of to force any call to it responding what you need, by adding this to your Capybara test:

before do
 allow_any_instance_of(Employee).to(
   receive(:picture_url).and_return('https://fullstacklabs.co/images')
 )
end

The before definition instructs the Capybara test set to run this part before any proper test (as defined by any it block), and can be included within either a context or a describe block. The allow_any_instance_of statement allows you to mock a class before it’s instantiated, and you can specify the method you want to mock using receive (in this case, picture_url) and the response you want the method to return.

You could also mock AssetsUrl, so that Employee still calls for it, but its response will be the one you actually want to mock, like so:

before do
 allow_any_instance_of(AssetsUrl).to(
   receive(:get_main_url).and_return('https://fullstacklabs.co/images')
 )
end

Furthermore, you could also mock the response for a method based on the parameters it’s being called with. Suppose you had a more generic get_url (instead of get_main_url) defined in AssetsUrl, which receives work_area, an area identifier that determines the asset’s base URL. Something like this:

class AssetsUrl
 # some code...
 def get_url(work_area)
   # some logic dependant on the specified work area
 end
 # ...more code
end

You might then want to test Employee for a particular work area. For example, you might want to test the Employee class for a worker in development, but then use the same test set to actually fetch the real data for the sales work area. To do so, you want to change them before the statement to something like this:

before do
 allow_any_instance_of(AssetsUrl).to(
   receive(:get_url)
     .with(:development)
     .and_return('https://fullstacklabs.co/development/images')
 )
end

By using the with method, you can instruct allow_any_instance_of to be called only when invoked with that particular parameter, just as you did with the receive method before. Whenever AssetsUrl gets invoked with any method other than get_url, or whenever get_url is called with a parameter other than developing, then the main definition will be executed (with no mocking behavior).

Finally, if you want to reset the original behavior for a particular test, you can always overwrite the mocking behavior with the original behavior using this line:

allow_any_instance_of(AssetsUrl).to(
 receive(:get_main_url).and_call_original
)

This line will just call the original method, ignoring any mocks defined in a before block.

Frequently Asked Questions