Will Taylor
Will Taylor

Reputation: 2304

How do I check if an API was called during a test

I want our app to speak to our microservice for sending emails via an API.

In tests (in RSpec), I want to say:

  1. Do something
  2. I expect the microservice to have been called / not been called.

For example:

  1. Create a user
  2. I expect to have sent the welcome email.

Upvotes: 2

Views: 2680

Answers (2)

max
max

Reputation: 101811

Sending real HTTP requests from your app can have some serious drawbacks:

  • Flapping tests due to connectivity issues.
  • Dramatically slower test suites.
  • Hitting API rate limits on 3rd party sites
  • Service may not exist yet (only documentation for it).
  • Non-deterministic responses can cause flapping tests.
  • API may not provide sandboxes or test mode.

You also need to weigh having clear boundaries of each application vs test acuity. Properly respecting the app boundaries means that you only test that your app communicates with the collaborator(s) as specified by the API(s) and stub out the actual communication.

The con is of course that stubbing always has a cost when it comes to test acuity. You may miss bugs where the API does not work as documented or where you're simply testing for the wrong thing.

Webmock for example lets you stub out the HTTP layer completely. You can set expectations on the external calls and mock the return values.

stub_request(:post, "api.example.com").to_return(status: 201)

expect(a_request(:post, "www.example.com").
  with(:body => {"a" => ["b", "c"]},
    :headers => {'Content-Type' => 'application/json'})).to have_been_made

VCR on the other hand is kind of a middle road, it lets you perform real HTTP requests which it records to YML files and then plays back the results. Subsequent runs are faster and deterministic. VCR is a lot less arduous than setting up mock responses, however you still need to deal with setting up the initial state and cleaning up any side effects on the external service from your tests.

VCR.use_cassette("synopsis") do
  response = Net::HTTP.get_response(URI('http://example.com'))
  expect(response.body).to match("Example domain")
end

This is an example plucked from a real world app that uses the Flickr API:

RSpec.feature 'Importing photosets from flickr' do

  include JavascriptTestHelpers

  let(:user) { create(:admin, flickr_uid: 'xxx') }

  before { login_as user }

  let(:visit_new_photosets_path) do
    VCR.use_cassette('photosets_import') { visit new_photoset_path }
  end

  scenario 'When I create a photoset it should have the correct attributes', js: true do
    visit_new_photosets_path
    VCR.use_cassette('create_a_photoset') do
      click_button('Create Photoset', match: :first)
      wait_for_ajax
    end
    find('.photoset', match: :first).click
    expect(page).to have_content "Showcase"
    expect(page).to have_content "Like a greatest hits collection, but with snow."
    expect(page).to have_selector '.photo img', count: 10
  end
end

Upvotes: 6

JCorcuera
JCorcuera

Reputation: 6834

You need to use message expectations, for example:

it "asks the project to trigger all hooks" do
  expect(project).to receive(:execute_hooks).twice
  expect(project).to receive(:execute_services).twice
  expect(project).to receive(:update_merge_requests)

  PostReceive.new.perform(...)
end

Regarding mailers check this other answer

Upvotes: 3

Related Questions