Getting Testy: Steps

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 finished test-driving our application from the outside in. We did
all of our unit testing in isolation using test doubles. We were
pretty careful to make sure that interfaces lined up properly by using
instance_double and friends. Now it’s time to see if the
application works properly when everything is hooked up.

We started this project by
writing some Cucumber specs.
Let’s get those running to see if our application is working
correctly.
As a reminder, here are the acceptance tests we ended up with:

Acceptance Tests

Feature: Currency Exchange

Allow the exchange of amounts of money from one

currency to another.

Scenario: basic currency exchange

Given the exchange rate for 1 USD is 0.91 EUR

When I convert 100 from USD to EUR

Then I should get 91.00 EUR

Feature: Currency List

Show the list of supported currencies in alphabetical order

by currency symbol.

Scenario: currency list

Given the following currencies exist:

|symbol|description|

|USD|UnitedStatesDollars|

|CAD|CanadianDollars|

|EUR|EuropeanUnionEuros|

When I ask for a currency list

Then I should see currencies and descriptions in this order:

|symbol|description|

|CAD|CanadianDollars|

|EUR|EuropeanUnionEuros|

|USD|UnitedStatesDollars|

Cucumber Step Definitions

We need to implement the step definitions for these tests.

The Given statements in both specs involve injecting currency data
into the system. We used VCR in our
unit tests to isolate them from the real external service. That seems
like a good approach here as well for the same reasons. We don’t
want our Cucumber specs to fail due to service outages or constantly
updating data.

The first decision we need to make is how we’re going to run our
application.

We want these specs to be as end-to-end as possible, which suggests
that we should shell out to our main executable file and capture the
output using %x{bin/currencyfx} or similar. However, VCR won’t
work this way - it doesn’t capture HTTP interactions that come from a
forked process like this.

There are some ways we could work around this issue, but let’s not go
there unless we later decide we need to.

The fallback position is to use our CLI object like we did with the
unit tests. This will run the application in-process allowing VCR to
do its thing, but still be very close to the outermost layer of our
application.

Running the Application

Let’s start with the When steps. That’s where we’ll run the
application, and also where we’ll get the VCR cassettes in place.

I’m not that fussy about the regular expressions I use in my Cucumber
steps; I’d rather something simple that generally works than the
perfect regex that no one can understand.

We’re assigning the returned output to an instance variable so that
our Then steps can examine the output and make assertions about it.
It’s not a good idea to overuse instance variables in Cucumber steps,
but we need some way of capturing the output for the Then steps to
check.

Rather than immediately figuring out how I’m going to run the
application and capture its output, I’m writing this step as if there
were already a run_application method that does exactly that.

We can now write the run_application method that we wish already
existed.

Running the Application

defrun_application(*args)

capture_output{Currencyfx::CLI.run(args)}

end

Once again, we can write the code assuming that there’s a handy
capture_output method available for our use. We’re using our CLI
object to do the work and using a
splat to collect all
of the arguments into an array to pass along, which is what will
happen when the real application uses ARGV.

We can write the When step for the currency list feature using the
same helper methods.

Currency List Step

When(/^I ask for a currency list$/)do

VCR.use_cassette("open_exchange_rates/currencies")do

@output=run_application("--list")

end

end

If we comment out the pending markers in our other step definitions,
we can run these specs and see where we’re at.

Integration Errors

We immediately get an error: NoMethodError: undefined method '/' for
"":String coming from our OpenExchangeRates class. We’ve written
all of our code to expect a number for amount, but our Cucumber step
is passing in a string.

This is why it’s a good idea to have a few end-to-end tests to make
sure everything is hooked up correctly.

We need to figure out where to fix this. Then we can write a
lower-level unit test and fix the bug. We have a few layers we can
choose from:

The Cucumber step

The CLI object

The Exchange object

The OpenExchangeRates object

When we run the program by hand from the command line, the amount is
going to be a string just like it is here. So the Cucumber step is
doing the right thing and has caught a real bug that would affect the
application in normal use.

It’s generally best to do all input sanitizing and normalizing at the
system boundaries. That way, the rest of the code can be written
confidently with the assumption that it will be called with the
correct arguments. Avdi Grimm’s excellent
Confident Ruby book (highly
recommended) has a great section title called “Guard the borders, not
the hinterlands”. By that guideline, the CLI object is the right
place for this work. Let’s look at our CLI specs again:

CLI Specs

context"when exchanging currency"do

let(:arguments){[100,"USD","EUR"]}

it"displays the converted amount and currency"do

allow(exchange).toreceive(:convert).with(100,"USD","EUR"){91.87}

expect{run_cli}.tooutput(/91.87 EUR/).to_stdout

end

end

This spec is written to pass in a number, not a string. Let’s change
that and see if we can get the unit test to fail.

Updated CLI Spec

context"when exchanging currency"do

let(:arguments){%w[100 USD EUR]}

it"displays the converted amount and currency"do

allow(exchange).toreceive(:convert).with(100,"USD","EUR"){91.87}

expect{run_cli}.tooutput(/91.87 EUR/).to_stdout

end

end

Here, we’ve updated arguments to take all strings using Ruby’s word
array (%w) syntax. Note that we don’t update the allow statement;
we’re saying that the CLI will take a string but will still pass a
number to the exchange. That’s exactly the behavior we want.

This spec now fails because exchange is receiving a message with
unexpected arguments. We update the code to convert the amount to a
Float and try again. The test passes and we can jump back out to
the Cucumber level.

Checking the Output

Let’s flesh out our Then steps. We’ve captured the program output
into the @output variable and we can use RSpec expectations to check
it.

Checking Conversion Output

Then(/^I should get ([\d.]+) (\w{3})$/)do|amount,currency|

expect(@output).tomatch/#{amount}#{currency.upcase}/

end

We don’t care about the exact format of the output; we just want to be
sure that it gives us the correct converted amount and the destination
currency. We can use the match matcher and construct a regex out of
the provided amount and currency.

Running the spec gives an error because our expected amount doesn’t
match. This is because we’re not yet injecting the exchange rate into
the VCR cassette, so we’re just getting back whatever the exchange
rate in the cassette happens to be.

Let’s write our other Then step and then deal with the injection
part.

This step is a bit trickier, because we need to take a table of
expected symbols and descriptions and match that against the output.
The step also says that order matters, so we can’t just do a bunch of
separate assertions.

Here’s one way to do it.

Checking Currency List Output

Then(/^I should see currencies and descriptions in this order:$/)do|table|

I’m not particularly pleased with this code, but I don’t have a better
option yet. We’re converting the table into a multi-line regular
expression that is then matched against the output.

To build the regex, we skip the header row of the table with
table.raw[1..-1]. For each row, we expect the symbol to appear,
followed by any amount of whitespace, followed by a vertical bar
(|), followed by more whitespace, followed by the description. We
then join the rows by expecting more whitespace and another vertical
bar to finish off the current row, any number of intervening rows, and
then a vertical bar and whitespace to start off the next expected row.

This spec also fails, but this time because our injected currency
descriptions don’t match the ones returned by the external service.

It’s time to figure out the injection issue.

Injecting Data

In order to keep these specs robust and repeatable, we need to be able
to inject fake data to be returned by the external service. As with
the specs we wrote for the OpenExchangeRates API, there is a bit of
danger here because we’re not using the external service directly.
However, it would be difficult to write a meaningful fast, robust spec
that used the real external service.

Once again, we’ll take advantage of VCR’s ability to use ERB templates
in the response.

Starting with the currency conversion test, we’ll edit our saved
cassette to replace the exchange rate for Euros with an ERB variable,
and then use the erb option to inject the exchange rate.

This is mostly simple ERB code, but the logic to place or not place
the trailing comma is a bit ugly. We’ll live with it for now.
currencies is a Hash, and we’re using Ruby’s
destructuring
feature to split each entry into its key and value (symbol and
description).

Fixing the Formatting Bug

The formatting bug isn’t really an integration bug; we just caught it
because we used a nice round number in our spec.

What happened is that we missed a couple of cases when we wrote the
unit tests for the CLI object. Let’s go correct that oversight now.

The obvious case we missed is when there are no cents in the converted
amount. But this also makes us think of the case where there are
fractional cents in the converted amount. We need to handle both
cases.

After writing tests for the new cases and doing a bit of refactoring,
we end up with this spec.

Missing Specs for the CLI

context"when exchanging currency"do

let(:arguments){%w[100 USD EUR]}

let(:converted){91.87}

beforedo

allow(exchange).toreceive(:convert).with(100,"USD","EUR"){converted}

end

it"displays the converted amount and currency"do

expect{run_cli}.tooutput(/91.87 EUR/).to_stdout

end

context"when the converted amount has fractional cents"do

let(:converted){91.86598}

it"rounds the converted amount to the nearest cent"do

expect{run_cli}.tooutput(/91.87 EUR/).to_stdout

end

end

context"when converted amount has no fractional amount"do

let(:converted){91}

it"still displays two decimal places"do

expect{run_cli}.tooutput(/91.00 EUR/).to_stdout

end

end

end

Notice how I’ve extracted a let for the converted amount, allowing
each context to override that to show exactly what’s different about
that context with a minimum amount of noise and duplication.

I could have wrapped the first it block in its own context, but I
couldn’t think of a good when statement for it, so I left it alone.

Both of these new specs fail. After a few simple changes in CLI, we
get them both passing. We then pop back out to the Cucumber specs and
they are all passing as well.

We now have a functional command-line application that implements both
of our features. Time to celebrate a bit!

Changes

OK, celebration over. Changes are coming.

With this currency exchange example, I’ve demonstrated the approach I
prefer to take when test-driving my applications. I’ve explained much
of my reasoning along the way, but let’s perform a couple of thought
experiments regarding potential changes to this application.

New External Service

If we need to switch to a new external service in order to get our
exchange rate data, we’d need to:

Test-drive a new subclass of API for the new service. This
subclass needs to implement the two methods defined by the abstract
API base class.

Modify the CLI’s default API to point at the new API.

Capture a set of VCR cassettes for the new API to use in the
Cucumber specs. The specs themselves won’t change, but we’ll have
to modify the step definitions to use the new cassettes. Depending
on how different the new cassettes are from the current ones, we
might have to tweak the ERB substitutions a bit.

That’s it.

The executable doesn’t change.

The Cucumber specs don’t change; the step definitions get a bit of
tweaking, but probably not much.

The CLI object only changes to point at the new API subclass.

Exchange and the API base class don’t change at all.

We add new code and specs for the new API subclass.

We add new VCR cassettes for the new API.

None of the existing unit tests change.

New Front End

If we need to replace our command-line front end with a web-based
front end, we’d need to:

Test-drive the web front end, using a test double of the Exchange
class. These tests would be similar in spirit to those for the
CLI object.

Modify the executable file to launch the server instead of running
the command-line application.

Modify the Cucumber step files to access the web front-end instead
of running the CLI. The When and Then steps are what would
likely need to change.

Again, that’s it.

The executable changes to start a server instead of running a
command-line.

The Cucumber specs don’t change; the step definitions do.

We add new code and specs for the web front-end.

Exchange, the API class, and its subclasses don’t change.

The VCR cassettes don’t change.

None of the existing unit tests change.

Overall, the code is pretty resilient to change. More importantly,
the tests are very resilient to change. We have to add specs for new
classes, but we are not changing any of our unit tests or Cucumber
specs to support these new features. That gives us a lot of
confidence that the changes we make don’t break anything.

Conclusion

This concludes the currency exchange example.

Throughout the example, I’ve been showing the development of the two
features in parallel. In a real development workflow, I’d have built
the entire currency conversion feature outside-in first, then I’d have
come back and built the entire currency list feature outside-in.

I developed them in parallel during this series just so I could
illustrate the various layers with a couple of examples.

I’ve made the code available
on GitHub. Feel free to
play with it. In order to run the actual application, you’ll have to
get your own API key for the Open Exchange Rates API. See the README
file for more details.