Technology

Mocking outbound http requests in go: you’re (probably) doing it wrong

It’s such a common scenario that most developers run into it within a few months of writing their first Go programs: your program makes HTTP requests to an external service to perform an everyday task, such as fetching a list of repositories from GitHub. As a responsible developer, you want to write unit tests without actually hitting the service with network requests.

You are also most likely coming to Go from other programming languages, and as a responsible developer, you’ve used a mocking framework like WireMock, nock, webmock, or something else in your language that allowed you to mimic external server responses. These helped to make you confident that your app was making the correct outbound requests and correctly handling different responses.

You figure this is a very common use case (and many other people have been in this situation) so surely the obvious way to do this in Go will be in the top few search results on Google. This is a perfectly reasonable assumption and often works. Unfortunately, what I’ve seen is that some good developers have come up with suboptimal solutions to the problem, shared their solutions with the community, and due to positive community reinforcement have dominated Google search results.

In fact, the Go creators already had devised a solution but didn’t optimize for the search engine (a bit ironic when Google is the shepherd of Go). Before we look at the native solution, let’s take a look at some common alternatives and their shortcomings.

A simple scenario

To illustrate, suppose we started working on a codebase and found the following untested function, and now we want to add a test.

https://gist.github.com/sonya/271b8a1f82d4c6a3a36731ec2d3cfbdc#file-contrived_scenario-go

There are a number of conditional branches in this function, but for now we just want to test the successful case. We want to make sure

  • We’re hitting the correct URL path
  • We’re passing the correct Accept header
  • We’re returning the string that is the value of the “value” key in the JSON response

Fashionable, but suboptimal: the interface approach

This approach will be familiar to those coming from duck-typed languages. This blog post by Sophie DeBenedetto does an excellent job explaining this approach (the post is actually quite good even though I disagree with the solution). In short, it works by mocking the built-in http.Client by defining an interface with a Do method, which is implemented by both http.Client and a mock version.

In the scenario we laid out, we need to change the source file so that Client is a package-level variable that implements the common interface:

https://gist.github.com/sonya/788779802ae320cd31ff761345fe6774#file-interface_approach-go

In the test file we have:

https://gist.github.com/sonya/adbe341c3df10370888e4115a8a6f3c7#file-interface_approach_test-go

How does this test do on the three assertions we wanted to make? It does everything we needed:

  • The handler checks the URL path and returns an error if it doesn’t match
  • The handler checks the Accept header and returns an error if if it doesn’t match
  • The return value of the function is checked against the string fixed, which we seeded in the mock handler

However, take note of how many changes we made:

  • Not only did we have to add new definitions to the source file, we actually had to alter the source code of the function we were trying to test
  • We are now exposing a HTTPClient as a package-level variable, which has a number of problems:
  • Anyone can read and write to it, which makes it hard to track down where the variable is being changed.
  • This variable exists for the sole purpose of enabling tests. Without looking at the test code, a developer might be confused about why it’s a variable
  • Also, because anyone can read and write to it, someone can accidentally or maliciously set it to something that breaks other parts of the program.

The package-level variable is an example of using the singleton pattern to manage state across tests, which can result in subtle bugs when not managed carefully. Suppose the external service you’re calling is actually a microservice under your control, and someone comes along trying to write an integration test for it. They run the service in a local Docker container on port 8080, make it return {“value":"actualvalue"}, and write this very simple test:

https://gist.github.com/sonya/b61ede65abf83b33e7716a55170b9288#file-interface_approach_integration_test-go

To their surprise, the tests fails with the message: Expected 'actualvalue', got fixed. They think there is a bug in their own service, but no matter what changes they make, the result remains unchanged. It might take them some extra hours of hair-pulling before they realize the global Client variable was stuck in the state your last unit test set it to.

Convenient, but suboptimal: the external library approach

There are third party Go libraries out there that provide APIs for mocking outbound HTTP requests, such as gock and httpmock. Both of these offer some of their own special sauce, but either way they still come with the overhead of a third party dependency.

httpmock

The httpmock library allows you to set up responders for a given request method and URL. It takes advantage of the fact that there is an existing global variable called http.DefaultTransport that configures how all newly created instances of http.Client make connections.

To test our sample function, we could write a test like this:

https://gist.github.com/sonya/99e23752f6cd967c6fe5596c646cee15#file-httpmock_test-go

In this example, the header and final value are checked the same way as the test where we mocked http.Client, whereas the URL path is checked implicitly by matching.

The matching approach makes test failures a bit harder to interpret because the error message will be the result of not running the responder, which might be something like invalid memory address or nil pointer dereference instead of the friendlier message Expected to request '/fixedvalue', got: %s in the previous version. We can address this using a regex in the responder and capturing what was called with httpmock.GetTotalCallCount().

The use of http.DefaultTransport is concerning here. Since http.DefaultTransport is overridden during the duration of the test, this means that when using httpmock, developers are unable to test any changes that they intentionally made to http.DefaultTransport in the source. Moreover, if someone forgets to call Activate() and Deactivate() at the beginning of the test, they could leave the http package in an undesired state for later tests.

gock

gock offers a feature-rich and concise API that includes matching of headers and path patterns. We can write a very concise test for our sample function:

https://gist.github.com/sonya/3ea4f2bcebfcaed2c8c3cf7be7f62f34#file-gock_test-go

This is appealing because of how short and readable it is. All three of our criteria are tested here: the path and header are implicitly tested through matching, and the return value is explicitly asserted at the end.

What I see as the main difficulty with this API is the issue with matching that we also saw in httpmock: if the source does not use the correct path and headers, the mock response is not triggered and the error message from the test failure will not give us information about which criteria we failed.

Note that if you have operations that need to make many separate requests, potentially to separate servers, a matching approach like gock is likely to be very useful. The use case we’re focusing on for this post is for operations that can be completed in at most a few requests to the same service.

There is an easier, idiomatic way

You do not need to define your own interface and mock classes. You do not need to depend on a third party library. The creators of Go foresaw the need to mock outbound HTTP requests a long time ago, and included an API in the standard library.

The API is provided in the package httptest, and there are many examples of how to use it, including not only in the httptest package’s own Godoc examples, but other golang contributed packages as well, including substantial unit tests in the token and clientcredentials tests in the oauth2 library.

Here is how we could write a test for our sample function:

https://gist.github.com/sonya/e1fc0d40ffaf6be2227302949478ce65#file-httptest_test-go

‍

Like the version where we mocked http.Client, we have full access to the original http.Request and thus can make direct assertions on what was requested. We don’t have to make any changes to the source file. We don’t have to go get any extra libraries. We don’t need to worry about shared state — even if we forget to call defer server.Close() we are unlikely to run into unexpected state issues elsewhere in the test suite. (In fact, intentionally calling server.Close() prematurely to simulate a remote connection hiccup is a simple way to test error handling.)

If you’re doing too much work, you’re probably doing it wrong

The Go language was developed at a time when communicating over HTTP was probably as commonplace as writing to the filesystem. As such, native support for testing HTTP was included as a first-class citizen in the standard library.

Unfortunately, information about httptest ended up too far below the fold — appearing in none of the intro or topical articles in the Go documentation page, nor in the blue book — thus evading many a developer’s browsing patterns. Developers coming from other languages where robust mocking libraries like WireMock and Mockito for JVM languages, nock for JavaScript, webmock for Ruby, etc. have been the norm, were likely primed to search elsewhere for a mocking library.

All of this means that if you didn’t know about httptest and have been using one of the non-native mocking approaches instead, it is perfectly understandable. However, the next time you find yourself having to test outbound HTTP calls, reach for the native approach instead and see how much time it could save you.

Interested in these types of discussions? Zus is hiring! Check out our current job openings.

Thanks to Anoush Mouradian, Brendan Keeler, and Bryan Knight for their invaluable feedback on this article.

Notes:

[1] Before I started writing production Go code, I happened to subscribe to the justforfunc channel on YouTube, which is run by one of the Go contributors. One of the videos was on httptest. The channel is a few years old, but pretty laid back and entertaining, and still relevant in my opinion.

[2] Code for the examples in this post are available at https://github.com/sonya/go-http-mocking

Mockingbird image courtesy of Sheila Brown (CC0)

Explore the latest content from Zus

Explore articles, case studies, succes stories and insights on modern health care.

Company
In the News

Zus Health Accepted as a Candidate QHIN Under TEFCA

Company
In the News

Zus Health Achieves HITRUST r2 Certification, Demonstrating Commitment to Cybersecurity and Information Protection

Company
Product

No Second Chances: How Zus Helps Providers Use AI to Get Risk Capture Right the First Time

Company
Product
Technology

Feature Fridays: Turning Condition Chaos into Clinical Clarity for Improved Care

Healthcare Disruption
In the News

Reimagining the EHR: From System of Record to System of Orchestration

Healthcare Disruption
In the News

The End of Retrospective Risk Adjustment: Why Prospective Coding Is the New Standard

Company
In the News

Zus Health Helps Value-Based Care Organizations Capture Accurate Risk with Intuitive, Evidence-Based Workflows

Company
In the News

The One Big Beautiful Bill Reforms Won’t Make Medicaid More Efficient, But Common Patient Records Will

Company
In the News
Technology

We Could Cut US Healthcare Costs in Half Tomorrow by giving it CPR…Common Patient Records.

Company
Product
Technology

Breathing Life into Patient Data: How the ZAP Brings Care to Life with CPR

Company
Product
Technology

Feature Fridays: Finding the Records Others Miss with Zus’ Record Locator Service

Company
Product
Technology

Feature Friday: The ZAP That Works Everywhere Care Happens

Company
Product
Technology

From Alerts to Action: Hospitalization Summaries that Keep Care on Track

Company
Product
Technology

Feature Fridays: Making Medication Management Smarter with Medication Journey Enrichment

Healthcare Disruption
In the News

Zus Health Recognized as Early Adopter in CMS Interoperability Framework

Company
Product
Technology

Feature Fridays: Keeping the Patient Record Always On with Intelligent Refresh

Company
Product
Technology

Feature Fridays: There’s a New CPR in Town that’s Changing how Care is Delivered

In the News
Technology

Feedback Zus shared with CMS: Building Smarter Infrastructure for Data-Driven Care

Company
Product

Designed for Safety, Built for Care: How Zus Develops Trusted Healthcare Technology

Company
Product

AI Replaces the Legal Pad: How Homeward is Prepping for Care in Half the Time

Company
Healthcare Disruption
People

Pipe Dreams: Converting ADT Noise into Transitions of Care Clarity

Company
Healthcare Disruption

Pipe Dreams: Building Smarter Healthcare Data Networks for Better Care

Company
In the News

Strengthening the Future of Value-Based Care: Reflections on Health Care Value Week

Company
Product

Zus Health Celebrates Unprecedented Growth, Announcing New Clients and AI-Powered Innovations

Technology

AI-Powered Insights: A New Chapter for Healthcare

Company

Championing Connected Care: Zus Joins Accountable for Health

Company
Technology

A Bridge to Better Care: Zus Health and Kno2’s QHIN Partnership

Healthcare Disruption
In the News

Falling Stars, Rising Results

Company
Healthcare Disruption
In the News

Bracing for the Presidential Election: Healthcare on the Political Divide

Company

Setting the Course for Always-On Care: Reflections from the Zus Summit

Product
Technology

The Gang Explains Information Blocking: Meaningful Use Era

People
Product

Why Zus: Building for (and with) our users

Technology

Build vs. Buy at Zus

Healthcare Disruption

Lessons from beyond the healthcare walls

People
Technology

Why Zus: The new wave of platforming is coming!

Healthcare Disruption
Technology

The Evolution and Death of the Electronic Medical Record

Healthcare Disruption

Moving away from top-down healthcare

Company
Healthcare Disruption

A great night in Houston

Company
Product

Zus Health Joins athenahealth's Marketplace Program to Bring Real-time Clinical Data to the Point of Care

Healthcare Disruption

On timeliness in healthcare data

Case Studies

Cecelia Health leverages Zus data throughout the end-to-end patient journey

Healthcare Disruption

Claims to Fame

Healthcare Disruption

Data Interoperability as HEDIS Helper

People

Fixing healthcare through data

Healthcare Disruption

What is value-based care?

People

Cooking with Zus

Healthcare Disruption

Making DaaS Mainstream

Healthcare Disruption
People

Q&A with Dave Boerner: On evolution vs. revolution

Company
Healthcare Disruption

Affirming our vision of a connected healthcare ecosystem

Healthcare Disruption

The life-giving of a down market

Company
Healthcare Disruption

The year is dead, long live the year

Healthcare Disruption

I’m a Mac, I’m a PC

Healthcare Disruption
Technology

Yes, data is oil, but my car runs on gas.

Healthcare Disruption

Interoperability. It’s electric!

Company

Announcing the new Zus look

Company
In the News

Zus Health Closes Financing, Signs Partnership with Elation Health, to Accelerate Growth of its Data Service to Provide Connective Tissue for Healthcare

Company
Product

Why should I use Zus?

Healthcare Disruption

The Gang Explains Information Blocking: HIPAA

Healthcare Disruption

A Song of Health and FHIR

Technology

Accelerate your developer onboarding with helpful git commit messages

Technology

State, coupling, complexity, code: four liabilities

Technology

Diagrams As Code In Your Repo’s README

Healthcare Disruption
Product

Chasms and Fires

People
Technology

So, why Zus?

Healthcare Disruption
People

Why Zus: I’m here to save the (healthcare) world!

Healthcare Disruption

Curing America's Healthcare

Company

Why Zus

Company
In the News

Zus and Healthie Partner to Empower Digital Health Organizations to Access Patient Health History

Company
In the News

Canvas Medical and Zus Announce Strategic Product Partnership

Healthcare Disruption

From boat show to prom night

No items found.

Zus Health Completes Another SOC 2 Type II Audit

Healthcare Disruption

Platform Do’s and Dont’s (from a former platform failure)

Healthcare Disruption

2023's on FHIR (but not R5)

In the News
Product

Zus x Healthie: In digital health, we’re all in this together.

Company
Technology

Maximizing Data: How Zus, Firefly and Elation Advance the Quest for Clinical Truth

Company
People

Inside the Zus Engineering Hiring Process

In the News
People

ACO Rx: A Strategic Shift From Claims to Real-Time Network Data

Case Studies

Zus Use: Real-time Patient Query in the ED

Company
Technology

Physicians Are Going in Blind: How Breaking Down EHR Silos Will Lead to Better Care

Company
People

The Challenges Providers Are Losing Sleep Over: A Roundtable Recap From the FLAACOs Conference