Getting Testy: APIs Part 2
This post is part of an ongoing series about Unit Testing.
Introduction
We’re building a simple command-line application that can do two things:
-
Convert an amount of money from one currency to another.
-
Show a list of all currencies supported by the application.
In
the last post,
we fleshed out the Exchange
class by defining an interface to an
external API. We did this in order to decouple our application from
the specific details of the API we ultimately choose to use.
Now it’s time to choose an API and implement that interface.
The API
After looking at a few options, I decided to start with the Open Exchange Rates API. It’s free for the limited use we need, and looks relatively simple to use.
It looks like there are two endpoints we’ll need:
-
/latest.json
gives us back the current exchange rates between all supported currencies and a base currency (USD). If we use a bit of simple math as outlined in the documentation, we can use these rates to convert an amount from one currency to another. -
/currencies.json
returns a list of all supported currencies. Each entry contains the currency code and a description of the currency.
If we later decide to purchase an enterprise license, there’s another
endpoint available (/convert
) that will perform the conversion for
us. Fortunately, the design of our abstract API
interface will
easily support that change.
Testing the API
We need to figure out how we’ll test our new OpenExchangeRates
API
class.
I won’t go through all of the options here, but there good blog posts that discuss some alternatives:
-
How to Stub External Services in Tests by Harlow Ward of thoughtbot
-
How to Test External APIs by Jared Carroll of CarbonFive
In general, we don’t want to rely on the external service when running our tests. It would slow our tests down significantly and also make them susceptible to any service outages. Also, the external service will constantly be providing new data as the exchange rates are updated; it’d be difficult to write a useful test that allows for that.
On the other hand, we want to be sure our system works properly with the external service, especially with its data format.
We’ll use VCR to help us. The first time we run the tests, VCR will download and save the actual response from the external service. After that, it’ll play back that same response without hitting the external service.
The Tests
With that decided, let’s start test-driving our concrete API interface.
Currency Conversion
Because the free version of the external service we’ve chosen only
returns a set of exchange rates, we have to do a bit of math to
implement the convert
method required by our abstract API.
At this point, we might be tempted to modify our API’s interface to
match that of the external service and do the math in our Exchange
object, but that would be a bad idea. Not all APIs will work this
way. By writing our API interface in terms of the services our
application needs, we can hide these kinds of details from the main
part of our application.
I often start with a “test list” as described by Kent Beck in his TDD By Example book. Sometimes I’ll write my list on paper or as comments in the test class, but in RSpec it’s just as easy to write the test list in the form of pending specs.
Let’s start with the first pending spec, a very simple conversion test. We’ll convert an amount from USD to USD. This is not much of a test, but it does allow us to get some structure in place.
I decided to be explicit about VCR cassette names instead of taking advantage of VCR’s ability to name cassettes based on the RSpec metadata. There are only two endpoints to hit, and we’re not sending any additional arguments. We should be able to re-use the same cassette for all of our conversion specs. This test is relatively trivial to pass and doesn’t actually need to hit the server at all; it can simply return the amount that was passed in.
Let’s flesh out a spec that makes us actually look at the exchange rates. The simplest case is to convert an amount from the base currency to another currency; to pass that test, we multiply the amount by the exchange rate for the target currency.
Since we don’t know what the actual exchange rate is going to be, we can use an expected value of 0 initially. After running the test, we can fill in the actual value based on the failure message after looking at the cassette that VCR saved to make sure it looks right.
I’m using the be_within
matcher here to avoid floating point
roundoff issues.
The next easiest spec to write is converting an amount to the base currency. We’ll divide the amount by the exchange rate for the source currency.
Since we already knew the exchange rate from the previous test, we can figure out the answer by hand and plug it in.
The last pending spec is to convert from one non-base currency to another.
For this one, we need to multiply by the target currency’s exchange rate and divide by the source currency’s exchange rate to get the answer. Again, we can look at the data in the stored cassette and perform the conversion by hand, then made sure the code gives us back the right answer.
Spooky Action at a Distance
As I read through these tests, I notice the magic numbers everywhere. All of the expected answers appear out of nowhere and the test doesn’t say much about where they come from.
This is a test of the interface to an external service so we do want to be sure that we’re communicating properly with that service, but these specs are not very understandable on their own.
Another issue is that all of these tests will likely fail if we ever decide to regenerate our cassettes. We may want to do that to be sure the external service’s response formats haven’t changed. However, updated cassettes will almost certainly have different exchange rates and that will make our tests fail. That’s not good.
VCR allows us to use ERB on the cassette files, so let’s give that a
try. We can edit the rates
cassette and replace a few of the rates
with ERB template strings. We can then modify the specs to inject
known exchange rates into the VCR response and update the expected
conversion values accordingly.
We can now see where the magic numbers are coming from, which is an improvement. But we still have the problem of our specs breaking when we next decide to refresh the cassettes. I don’t have a good solution to this problem, other than to make a note about what changes to make to the cassette when it is refreshed.
Currency List
The spec for the currency_list
function is much simpler.
Note that we’re not testing the entire result. We’re testing the
minimum amount that will give us confidence that we’re correctly
talking to the external service, and that we’re returning the right
kind of result. RSpec’s include
matcher is perfect for this; as
long as the result is a Hash
that contains at least the key/value
pairs we’re asking for, the test will pass.
The Jasmine testing framework in JavaScript has some similar matchers that I’ve written about before.
In general, I try not to over-constrain the code I’m testing; I don’t want my tests breaking all the time due to irrelevant details. This is a good example of that principle. We can test a couple of currencies that aren’t likely to change in the near future and not worry about our test being too fragile.
Conclusion
We’ve now worked our way to the inside of our system, one layer at a time. If we’ve done everything right, we should now have a working application. But we haven’t yet tested all of the pieces as an integrated whole.
As you may recall, we started out by writing some Cucumber tests. Our next step is to get those Cucumber tests running to see if we’ve got our application working end-to-end. That will be the topic of the next post.