Mock responses with Stash

Oct 9

So a few posts ago, we looked at how to abstract dependent modules in Elixir to make testing easier. That post and its follow up attempt to implement a pattern suggested by Jose Valim, which he called explicit contract. This pattern has the following characteristics:

  1. An interface module is created to wrap around the dependency.
  2. A test version of the interface module is created.
  3. Test responses are written in the test interface

Our implementation of this in the near earth wraps the HTTPoison library. There are some issues with this, but before we get to those I want to look at how we're building and retrieving mock responses. Right now those responses are built into the test module.

This post is concerned with moving our test responses out of the test interface module and into the tests themselves. This is useful because we don't have to move from file to file while we're working on the tests. It should simplify our test code somewhat.

To do this, we're going to use a library called Stash, which is a wrapper around ETS, Elixir's build in key-value store. Adding Stash is simple, of course we add stash to our mix.exs file:

              defp deps do
                [
                  {:env_helper, "~> 0.0.4"},
                  {:httpoison, "~> 0.10.0"},
                  {:poison, "~> 3.0"},
                  {:stash, "~> 1.0"}
                ]
              end
              

And then change the implementation of test/support/http.ex. Previously, we only had a mock response for the 404 case:

              defmodule NearEarth.TestHttp do
                use HTTPoison.Base
              
                def get("https://api.nasa....?api_key=DEMO_KEY") do
                  {:ok, %HTTPoison.Response{headers: [],
                                            status_code: 404}}
                end
              
                def get(args) do
                  HTTPoison.get(args)
                end
              end
              

But now we're going to redefine the get method to use stash:

              def get(route) do
                Stash.get(:near_earth, route)
              end
              

So whatever value is stored in the ETS table :near_earth for the key route, that's what the get method will return.

Obviously, to get our current tests working we're going to need to do some work. Right now, we have two tests. The first tests our response to a 404, the second actually makes a real call out to the near earth api to get an asteroid and then tests the parsing of that JSON file. To get the 404 test working, we add a response to the setup:

              setup do
                not_found = %HTTPoison.Response{status_code: 404}
                {:ok, %{not_found: not_found}}
              end
              

And then add that response to the :near_earth stash table in the test:

              test "will return not found if 404", context do
                Stash.set(:near_earth, 
                          "#{Sets.neo_url}fakevalue?api_key=#{Sets.api_key}",
                          {:ok, context.not_found})
                assert {:error, :not_found} == 
                       NearEarth.get_asteroid("fakevalue")
              end
              

So we've set the value of "#{Sets.neo_url}fakevalue?api_key=#{Sets.api_key}" to be an HTTPoison response with a status of 404. The Sets module is where I'm defining helpers to application and environment variables using the env_helper library.

The get_asteroid call uses the HTTP module:

              def get_asteroid asteroid do
              url = "#{Sets.neo_url}#{asteroid}?api_key=#{Sets.api_key}"
                case Sets.http.get(url) do
                  {:ok, response = %{status_code: 200}} -> 
                    Poison.decode(response.body)
                  {:ok, %{status_code: 404}} ->
                    {:error, :not_found}
                end
              end
              

And because we're using the test http module in our test environment, we get back the fake 404 response and the test passes.

A similar strategy will work for getting the asteroid, although in that case we will need a fake asteroid JSON response to work with. My solution to this, for the moment, was to add that as a json file in the test/support folder and then add a module stubs:

              defmodule NearEarthTest.Stubs do
                def asteroid do
                  {:ok, asteroid} = File.read("test/support/asteroid.json")
                  asteroid
                end
              end
              

Add the stub to the test setup:

              asteroid_response = %HTTPoison.Response{status_code: 200, 
                                    body: Stubs.asteroid}
              
              {:ok, %{asteroid: asteroid_response,
                      not_found: not_found}}
              

And then put that in the test:

              test "retrieves asteroid by designation", context do
                id = "3542519"
                Stash.set(:near_earth, 
                          "#{Sets.neo_url}#{id}?api_key=#{Sets.api_key}",
                          {:ok, context.asteroid})
                {:ok, asteroid_response} = NearEarth.get_asteroid(id)
                assert asteroid_response["name"] == "465633 (2009 JR5)"
              end
              

And again, we should have a passing test.

One obvious problem with this implementation is that I'm making everything very tightly coupled to the HTTPoison library. In a future post, we'll change the http module so that this coupling disappears, but for now we'll live with it.

We've refactored, and now we want to add an endpoint. In particular, I'd like to get all the asteroids that will make their closest approach today. Fortunately, NASA has a useful /feed endpoint that takes a start and end date as arguments. To write the failing test, we'll add another library to our codebase. Faker is a good library for generating fake data of various kinds, such as dates, addresses, and of course styles of beer. We use Faker at work mostly for dates, addresses and to generate fake car make and model names. Here, we're interested in the date functionality.

We'll also want to add another stub, this one with a real JSON response from the feed api. We'll again use the Stubs module to import the file, and add the stub to the test setup:

              asteroids_response = 
                %HTTPoison.Response{status_code: 200, 
                                    body: Stubs.asteroids}
              {:ok, %{asteroids: asteroids_response,
                      asteroid: asteroid_response,
                      not_found: not_found}}
              

And then test against the stub by adding the fake response to stash:

              test "retrieves today's asteroids", context do
                today = Date.utc_today
                        |> Date.to_string
                tomorrow = Faker.Date.forward(1)
                           |> Date.to_string
                Stash.set(:near_earth, 
                  "#{Sets.neo_url}feed?start_date=#{today}&end_date=#
              {tomorrow}&api_key=#{Sets.api_key}", 
                  {:ok, context.asteroids})
                {:ok, asteroids} = NearEarth.get_today()
                assert asteroids == context.asteroids.body 
                                    |> Poison.decode!
              end
              

Since we haven't implemented get_today at all, it's pretty clear we're going to get a failing test here. So, we'll get it passing:

              def get_today do
                today = Date.utc_today()
                        |> Date.to_string()
                tomorrow = :calendar.universal_time
                           |> :calendar.datetime_to_gregorian_seconds
                           |> (&(&1 + 86400)).()
                           |> :calendar.gregorian_seconds_to_datetime
                           |> elem(0)
                           |> Date.from_erl
                           |> elem(1)
                           |> Date.to_string 
                case Sets.http.get("#{Sets.neo_url}feed?start_date=#{t
              oday}&end_date=#{tomorrow}&api_key=#{Sets.api_key}") do
                  {:ok, response = %{status_code: 200}} -> 
                    Poison.decode(response.body)
                end
              

Granted, that could be the most convoluted method ever of getting a date string for tomorrow, but I think that this one will at least work. As always, a working version of this code is available on github.