Gustav Ehrenborg fullstack developer

Email symbol LinkedIn logo GitHub logo Instagram logo

Basic mocking in Rails using RSpec Mocks and WebMock Faking service and API calls in Ruby on Rails

Thursday, November 22, 2018
The service

I was invited to do a short speech to students at the university where I once studied. It was a recruitment event that the company I work for promoted themselves. I had about 20 minutes and I wanted to make it 20 good minutes and teach something that is useful and that I didn't learn at the university. I decided to give a brief introduction to mocking in testing, more specific in Ruby on Rails, using RSpec and WebMock.

If you have an API client, a service using it and a test for testing the service, like this:

class PlaceholderClient
  include HTTParty
  base_uri 'https://jsonplaceholder.typicode.com'

  def get_photo(id)
    response = self.class.get("/photos/#{id}")
    JSON.parse(response.body)
  end
end

class PhotoService
  def create_photo(photo_id)
    photo_hash = PlaceholderClient.new.get_photo(photo_id)
    photo = Photo.new(photo_hash.slice('title', 'url'))
    photo.save!
    photo
  end
end

RSpec.describe PhotoService do
  it 'creates a photo' do
    service = PhotoService.new
    photo = service.create_photo(1)
    expect(photo).not_to be_nil
    expect(photo.title).not_to be_nil
    expect(photo.url).to be_url
  end
end

The test will test the service just fine, and it will test the client and it will test the internet connection and it will test the API too... This is not what we want. Tests should be isolated and test one thing at a time. To fix this, we introduce mocking. The service is rewritten to accept a client as a parameter.

class PhotoService
  def initialize(client = PlaceholderClient.new)
    @client = client
  end

  def create_photo(photo_id)
    photo_hash = @client.get_photo(photo_id)
    photo = Photo.new(photo_hash.slice('title', 'url'))
    photo.save!
    photo
  end
end

The test is then changed into injecting a client to the service, but not any client, a mocked client. This new mocked client is suppose to be called once with the method name get_photo and it will then return the same data as the real client. If it's not called, the test will fail. Now this test is isolated to test just the service, not anything else.

it 'should create a photo' do
  client = double
  allow(client).to receive(:get_photo)
                        .with(kind_of(Numeric))
                        .and_return({ title: 'An image', url: 'http://www.example.com/image.png'}.stringify_keys!)

  service = PhotoService.new(client)
  photo = service.create_photo(1)
  expect(photo).not_to be_nil
  expect(photo.title).to eq('An image')
  expect(photo.url).to eq('http://www.example.com/image.png')
end

But if we actually want to test the actual PlaceholderClient, not the service, how would we go about, you ask. A test of the client could look like the following:

require 'rails_helper'

RSpec.describe PlaceholderClient do
  it 'should fetch photo' do
    json = PlaceholderClient.new.get_photo(1)
    expect(json['title']).not_to be_nil
    expect(json['url']).to be_url
  end
end

But here we are again, your internet connection fails and this test fails, or the API is temporary unavailable and the test fails. This is not what we wanted. However, there a solution for that too. Enter WebMock. https://github.com/bblimke/webmock

Configure WebMock to return predefined data on certain http calls in the spec_helper.rb file. The following lines will make all get requests to any url that matches the pattern /jsonplaceholder.typicode.com/ to return a body with a title and a url in json format. Great! WebMock will disable all HTTP calls on default, so we don't have to do any changes to the test above. It will now not rely on your internet connection or other things.

config.before(:each) do
  stub_request(:get, /jsonplaceholder.typicode.com/)
    .to_return(status: 200,
               body: {
                 title: 'An image',
                 url: 'http://www.example.com/image.png'
               }.to_json)
end

It is, however, a good habit to test the actual things sometimes, and this is called integration test. The following code shows a nearly identical test to the one above, but this is tagged as an integration test and it will disable WebMock, that means enable http connections.

RSpec.describe PlaceholderClient, :integration do
  before(:each) do
    WebMock.disable!
  end

  it 'should fetch photo' do
    json = PlaceholderClient.new.get_photo(1)
    expect(json['title']).not_to be_nil
    expect(json['url']).to be_url
  end
end