Getting Testy: APIs Part 1
This post is part of an ongoing series about Unit Testing.
Introduction
Recall that 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
previous post on testing from the outside in,
we test-drove a CLI
object that formed the basis of our command-line
interface. Along the way, we created the skeleton of an Exchange
class that actually does the work for us.
Let’s move in a level and start test-driving the implementation of the
Exchange
class.
External Services
At some point, we’re going to have to use an external service to help us in our task. There are a number of options available to us, but we’re not yet sure which one we might choose.
External dependencies are one of the things that are most likely to change in an application. An external provider might go out of business. Our boss might meet someone at a conference and sign a deal to integrate with another company’s service. Our application might outgrow an existing service.
Because of this, we need to isolate our application from that choice as best as we can. We want to make sure that we can switch to a different service without causing ourselves undue pain. By delaying our choice of which external service to use, we’re forcing ourselves to design our application in a way that makes it easy to switch services later.
Hiding Behind an Interface
In order to accomplish this goal, we’ll hide the external service behind an interface that we define and control. We’ll write an implementation of that interface for any external service we choose to support, adapting it to the API provided by the external service.
One advantage of this approach is that we can mock or stub the interface in our code when testing the objects that talk to it. When doing mock-based testing, a common piece of advice is “don’t mock what you don’t own”. Since we’re defining the interface, we own it and we can mock it out when necessary.
What should the interface look like? We want it to meet the needs of our application, and yet be easy to adapt to whatever external API we want to use.
The services we need from the external API are really the same services
provided by our Exchange
object: convert an amount from one currency
to another, and return a list of supported currencies. The method
signatures of our API interface will likely be identical to those of
the Exchange
class. This won’t always be the case when working with
an external API.
What should the API interface return? For the convert
method, it
makes sense to return a floating point number just like Exchange
does. But for the currency_list
method, it seems likely that any
API will return a hash of currency codes and descriptions; we’ll want
Exchange
to turn that into the format needed by the ListFormatter
class we identified last time.
Converting Amounts
Let’s start by test-driving Exchange
’s convert
method.
We start out by creating the Exchange
instance as our subject and
injecting a test-double for the API
interface. Because we’re using
instance_double
again, this forces us to start creating a skeleton
for the API
interface.
Notice that I’m using an
abstract method affordance
here since I expect the API
class to be an abstract base class for
all API implementations.
We don’t strictly need this interface in Ruby, because we can just use its duck-typing capabilities. But in this case, having the interface explicitly defined communicates to future developers what an API implementation needs to look like. And we can use RSpec’s verifying doubles to catch us if we make a change somewhere. An alternative would be to use an explicit role instead of an abstract base class.
The API’s convert method is a query method. As we discussed last time, we don’t really need to test that it gets sent; we just need to stub it out to make our tests pass. However in this case, I wanted to explicitly communicate that I’m forwarding all of the arguments on to the API method without any adjustments or manipulation, so I felt it was worth the extra test in this case.
Obtaining a Currency List
Now let’s test-drive Exchange
’s currency_list
method. As mentioned
above, we expect to get back a hash of currency codes and
descriptions. What we’d like to return from Exchange
is something
more useful, perhaps a sorted array of Currency
objects. Let’s
write the spec that way.
Note that I’m testing the two parts of the currency_list behavior independently.
First, I’m testing that it turns the returned hash into an array of
Currency
objects. By using the match_array
matcher, I’m saying
that I don’t care about order; I just care that I get the objects I’m
interested in. I’m imposing the minimum number of constraints I can
while remaining confident in the behavior of my code. If I later
choose to sort the currencies differently or not at all, this
test won’t break.
Second, I’m testing that the array is sorted by currency code. I’m not repeating all of the currency objects again; I’m just pulling out the codes and checking those. This makes for a much clearer test. There isn’t extra noise distracting me from the main point I’m trying to communicate.
These specs involve a bit more implementation than normal. We don’t
actually have a Currency
class yet, so as I worked on implementing
the code in Exchange
, I marked these specs as pending while I
test-drove the Currency
class into existence.
I could have used a test-double instead, but Currency
is a
relatively simple
value object and I try
to use those directly. Value objects don’t tend to have dependencies,
and they’re usually cheap to create and use. In this case, Currency
objects know how to compare themselves to each other, and that kind of
behavior is tedious to mock and the resulting tests are very hard to
understand.
Naming Specs
Note the two main describe
blocks in the specs above:
Many people would have written those as describe "#convert"
and
describe "#currency_list"
since those are the methods being tested.
People will often test every public method of the class independently,
each with its own top-level describe
block named after the method
under test.
I understand that pattern and its attractiveness, but I advise against it for a few reasons:
-
I always want to test the responsibilities of my objects, not the methods that implement those responsibilities. One responsibility might consist of several methods; I’ll test those methods as a group rather than individually.
-
I want to keep implementation details out of my specs as much as possible. Method names are definitely implementation details.
-
If I want to see where a particular method is tested, I can easily find where it is called in the specs. As with (good) comments, the
describe
block gives me an opportunity to express additional information about my intentions. I don’t want to waste that opportunity by repeating information (the method name) that is already in the code.
I will sometimes make an exception to this advice when writing the specs for an API. In that case, the specs provide a form of documentation for the API, and having that documentation organized by API endpoint makes sense. But for objects in my system, I care more about their responsibilities (what do they do) than their API (how do they do it).
Conclusion
We’ve now taken another step inside our application as we work from outside-in.
This application is simple enough that we might consider merging
Exchange
and API
into one class. But in more complicated
situations, this is a good division of responsibilities. Even in this
simple case, having a well-defined abstract interface communicates
something about the design of the application. Exchange
is the main
“business logic” class; it gets help from some kind of external API to
do its job. We can shuffle responsibilities between those two objects
without affecting the rest of the application.
The main thing is to try to keep our application very decoupled from
the details of any particular external API. By defining our API
interface in terms of the services we expect, we can enforce that
decoupling.
We’ll make the implementation that talks to a particular API do a bit of work to conform to our expectations. There’s always some give and take here. We don’t want to define the interface in a way that makes it difficult to write an adapter for any particular API. But we also don’t want to force our application into a shape that is only suited to one particular API.
In the next post, we’ll implement an adapter for an external API.