Subscribe to access all episodes. View plans →
Published September 30, 2019
Elixir 1.8
Phoenix 1.4
Source code on GitHub
Here here we have a module that fetches the current price of a cryptocurrency. Let’s see it in action. We’ll start an iex
session with our project. We see we get an OK tuple returned if we’re able get the price successfully and an error tuple if it’s not able to fetch the price.
$ iex -S mix
Interactive Elixir (1.8.1)
> Teacher.Coins.CoinService.fetch_current_price("bitcoin")
{:ok, 9550.597079822928}
> Teacher.Coins.CoinService.fetch_current_price("elixircoin")
{:error, "No coin found"}
In this episode let’s add some tests for our fetch_current_price
function. Because this functions calls to the CoinCap API to get the current price or a cryptocurrency we need to determine how we want to handle this in our test. Let’s use the ExVCR package to help test this. ExVCR allows us to record HTTP requests and responses and then replay them in our tests. This way we wont hit the API every time we run our test and we get the full response we get in production.
To start let’s go to Hex and get the exvcr
package. Then let’s open our mixfile and add it to our list of dependencies.
mix.exs
...
{:exvcr, "~> 0.10.4", only: :test},
...
Then let’s go to the command line and install it.
$ mix deps.get
...
New:
exactor 2.2.4
exjsx 4.0.0
exvcr 0.10.4
jsx 2.8.3
meck 0.8.13
Let’s create our test file. Since our CoinService
module is in the “coins” directory, let’s match that in our “test” directory. We’ll create a new “teacher” directory and then a “coins” directory inside of it. Then we’ll create our new test file coin_service_test.exs
and define our test module, using ExUnit.Case
.
Now let’s setup our test to use ExVCR to mock the API call. We’ll include use ExVCR.Mock
and by default ExVCR uses ibrowse
, but we are already using the HTTPoison package, which uses Hackney. So instead of bringing in another dependency let’s configure ExVCR to use Hackney. Then let’s add a setup block and start HTTPoison from it.
We can now start writing our first test. We’ll add a describe block for CoinService.fetch_current_price
, then let’s test that we are able to fetch the current price of a cryptocurrency. We’ll call our function to get the current price of a Bitcoin, pattern matching on the OK tuple that’s returned and then assert that the returned price matches the current price.
To get the current price, let’s run the fetch_current_price
function and use the current price that’s returned. Because the price returned from this function changes depending on what’s returned from the API, we need to tell ExVCR to record the API request and response. To do that we’ll wrap the inside of our test with the use_cassette
macro we get from ExVCR. This takes a name, so let’s call the cassette for this test price_fetch_success
. The first time we run our test, this will call the API and record the request and response as a cassette. And then once the cassette is recorded every subsequent time we run our test it will then use that cassette instead of calling the API.
test/teacher/coins/coin_service_test.exs
defmodule Teacher.Coins.CoinServiceTest do
use ExUnit.Case
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
setup do
HTTPoison.start
:ok
end
describe "CoinService.fetch_current_price/1" do
test "current price fetched" do
use_cassette "price_fetch_success" do
{:ok, price} = Teacher.Coins.CoinService.fetch_current_price("bitcoin")
assert price == 10030.066907869972
end
end
end
end
Now that we have our test, let’s go to the command line and run it.
$ mix test test/teacher/coins/coin_service_test.exs
...
Our test fails here because the price that was returned from the API is different that what we specified in our test.
Let’s check to see if our cassette was created. Looking at our project we see there’s a new directory named “fixtures/vcr_cassettes”. This is where our ExVCR cassettes are stored by default. If we open our “fetch_price_success.json” cassette we can see some data about the request, like the URL, the response body, headers, and status code.
Let’s grab the price that was recorded from the test and then let’s update our test to use it. Now if we go to the command line and run our test again - great, it passed. And now every time we run this test instead of making a new call to the API, the response recorded in the cassette is used.
$ mix test test/teacher/coins/coin_service_test.exs
.
Finished in 0.6 seconds
1 test, 0 failures
Randomized with seed 736052
Now let’s add another test. We’ll go back to our coin_service_test.exs
module. And we’ll add another test for instances when this function is called with a coin that doesn’t exist.
Since we know this will call the API, let’s create a cassette named “price_fetch_error” for the test to use. Then let’s call our function again with a cryptocurrency that doesn’t exist pattern matching on the {:error, ...}
that’s returned and we’ll assert that we get the “No coin found” message returned.
test/teacher/coins/coin_service_test.exs
defmodule Teacher.Coins.CoinServiceTest do
use ExUnit.Case
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
setup do
HTTPoison.start
:ok
end
describe "CoinService.fetch_current_price/1" do
test "current price fetched" do
use_cassette "price_fetch_success" do
{:ok, price} = Teacher.Coins.CoinService.fetch_current_price("bitcoin")
assert price == 10030.066907869972
end
end
test "invalid coin used" do
use_cassette "price_fetch_error" do
{:error, msg} = Teacher.Coins.CoinService.fetch_current_price("elixircoin")
assert msg == "No coin found"
end
end
end
end
The we’ll go back to the command line and let’s run only our new test, which we can do by adding the line that our test starts on.
$ mix test test/teacher/coins/coin_service_test.exs:18
Excluding tags: [:test]
Including tags: [line: "18"]
.
Finished in 0.5 seconds
2 tests, 0 failures, 1 excluded
Randomized with seed 598613
It looks like our test passed, but let’s check that a new cassette was recorded. Let’s open the “fixtures/vcr_cassettes” directory and great, our “price_fetch_error.json” cassette was recorded. Now let’s look at some other features of VCR.
We can use $ MIX_ENV=test mix vcr --help
to list all the different tasks provided. There’s mix vcr
to list all of the cassettes, mix vcr.delete
to delete a given cassette, mix vcr.check
to check the cassette use for a given test, and mix vcr.show
to show the contents of a given cassette. Let’s use mix vcr.delete
to delete our “price_fetch_error” cassette.
$ MIX_ENV=test mix vcr.delete price_fetch_error
Deleted price_fetch_error.json.
ExVCR has a few different options as well, which you can find in the README. Let’s try one configuration option.
Back in our coin_service_test.exs
let’s change the directory that we store the cassettes for these tests in. In our setup
block let’s add a call to ExVCR.Config.cassette_library_dir
passing in the new directory we want these cassettes stored in. Let’s use a directory named “coin_cassettes”.
test/teacher/coins/coin_service_test.exs
...
setup do
HTTPoison.start
ExVCR.Config.cassette_library_dir("fixture/coin_cassettes")
:ok
end
...
Then let’s go back to the command line and delete our other cassette.
$ MIX_ENV=test mix vcr.delete price_fetch_success
Deleted price_fetch_success.json.
Then run our tests again to record new ExVCR cassettes.
$ mix test test/teacher/coins/coin_service_test.exs
.
1) test CoinService.fetch_current_price/1 the current price is fetched (Teacher.Coins.CoinServiceTest)
test/teacher/coins/coin_service_test.exs:12
Assertion with == failed
code: assert price == 10024.177762984644
left: 10028.88666677563
right: 10024.177762984644
stacktrace:
test/teacher/coins/coin_service_test.exs:15: (test)
Finished in 2.4 seconds
2 tests, 1 failure
Randomized with seed 862708
Our test checking that the price is returned fails, which is expected since we are calling the API directly and the price has changed.
Let’s check the cassette location is correct - and great the new “/coin_cassettes” directory has been created and our cassettes are there.
Let’s go back to the command line and grab the new price. Then we’ll go back to our test and update it and now if we run our tests again - they pass.
$ mix test test/teacher/coins/coin_service_test.exs
..
Finished in 1.2 seconds
2 tests, 0 failures
Randomized with seed 855551