The only HTTP this app makes

Free preview 4 min to read

Before learning tools for stubbing outbound HTTP, it is worth knowing how much outbound HTTP there is. In this repository, a search for every HTTP client returns two files.

$ grep -rln "Net::HTTP\|Faraday\|HTTParty\|RestClient\|URI.open" app/ lib/
app/services/partners/fetch_site_icon_service.rb
app/services/partners/webhook_transport.rb

Two HTTP callers, plus an OAuth flow through a gem, plus outbound mail. Four integrations in an application of 204 spec files, which is normal. And it is why "how do we handle external services in tests" is usually a smaller question than it sounds.

What the four are

Partners::FetchSiteIconService. Given a partner's URL, it fetches the page, reads the <link rel="icon"> and apple-touch-icon tags, picks the largest, downloads it, and attaches it as the partner's logo.

def self.call(partner:, http: Net::HTTP)
  new(partner, http).call
end

Read the signature. The HTTP client is a parameter with a default.

Partners::WebhookTransport. A thin Net::HTTP adapter that posts a signed event to a partner's endpoint, with five-second timeouts, raising DeliveryError on any non-2xx response so the job can retry. It exists as its own module so the delivery job can be tested without the network.

GitHub OAuth, through omniauth-github:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, ENV["GITHUB_KEY"], ENV["GITHUB_SECRET"], scope: "user:email"
end

Outbound mail, through Action Mailer and SMTP.

Why the network must not be in the suite

A test that makes a real request is worse in four separate ways, and they compound:

It is flaky. The service is down, rate-limits you, or is slow this morning. The test fails and nothing in your code changed, which teaches everyone to re-run the suite rather than read the failure.

It is slow. A network round trip is tens or hundreds of milliseconds against the roughly 0.05ms a local database query costs. One integration test can dominate a suite.

It is not repeatable. The response changes. A test asserting on today's data fails tomorrow.

It has side effects. A test that POSTs to a real service creates real records, sends real messages, or spends real money.

flowchart TB
  T["a spec that makes a real request"] --> A["flaky: the service, not your code"]
  T --> B["slow: tens of ms, or a timeout"]
  T --> C["not repeatable: the response changes"]
  T --> D["side effects: real records, real money"]
  A --> R["the suite stops being evidence"]
  B --> R
  C --> R
  D --> R

How this repository handles each one

Four integrations, four techniques, and none of them is WebMock or VCR:

Integration Technique Where
Site icon fetch The HTTP client is injected; the spec passes a fake class fetch_site_icon_service_spec.rb
Partner webhooks The job's spec stubs WebhookTransport.post with RSpec mocks; the adapter itself has no spec deliver_webhook_job_spec.rb
GitHub OAuth OmniAuth.config.test_mode = true and mock_auth[:github] omniauth_callbacks_spec.rb
Mail Action Mailer's :test delivery method Rails default

That is a deliberate set of choices rather than an oversight. And the rest of this course is about when each is right and what the two tools everyone reaches for add.

The question underneath

Every technique here is answering one question: at which layer do you cut the wire?

You can replace the object that makes the call, ask the library to pretend, intercept the HTTP at the socket boundary, or replay a real response recorded earlier. Each cut is at a different depth, and the depth determines how much of your code is still being tested.

That framing is lesson 2, and it is more useful than either tool.

Key takeaways

  • This application makes outbound HTTP from two places (site icons and partner webhooks), plus OAuth and mail.
  • Real network calls in a suite are flaky, slow, unrepeatable and side-effecting: four independent reasons.
  • FetchSiteIconService takes its HTTP client as a parameter: http: Net::HTTP.
  • The four integrations use four techniques: injection, a stubbed adapter, the library's test mode, and Action Mailer's :test delivery.
  • The organizing question is at which layer you cut the wire, because that decides how much code is still tested.
Lesson 1 of 7 · free

Keep going with lesson 2

Four places to cut the wire

Compares four places to cut the wire: injected fakes, library test modes, WebMock and VCR, and what each proves.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...