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:

  1. Convert an amount of money from one currency to another.

  2. 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.

Testing Exchange::convert
module Currencyfx
RSpec.describe Exchange do
subject(:exchange) { described_class.new(api: api) }
let(:api) { instance_double(API) }
describe "converting currency amounts" do
it "forwards conversion requests directly to the API" do
expect(api).to receive(:convert).with(100, "USD", "EUR")
exchange.convert(100, "USD", "EUR")
end
it "returns the converted amount directly from the API" do
allow(api).to receive(:convert) { 42.00 }
expect(exchange.convert(100, "USD", "EUR")).to equal(42.00)
end
end
end
end

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.

The API Interface
module Currencyfx
class API
def convert(amount, source, target)
fail NotImplementedError, "Subclasses must implement this"
end
end
end

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.

Testing Exchange::currency_list
describe "retrieving a currency list" do
let(:api_result) do
{
USD: "United States Dollar",
CAD: "Canadian Dollar",
EUR: "Euro"
}
end
before do
allow(api).to receive(:currency_list) { api_result }
end
let(:currency_list) { exchange.currency_list }
it "converts the returned Hash into an array of currencies" do
expected = [
Currency.new("USD", "United States Dollar"),
Currency.new("CAD", "Canadian Dollar"),
Currency.new("EUR", "Euro")
]
expect(currency_list).to match_array(expected)
end
it "sorts the array by currency code" do
expect(currency_list.map(&:code)).to eq(%w[CAD EUR USD])
end
end

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:

Top-Level Describe Blocks
describe "converting currency amounts" do
# ...
end
describe "retrieving a currency list" do
# ...
end

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.