This post is part of an ongoing series about Unit Testing.

Within our applications, there are often computations that can be expressed as pure functions; that is, methods whose return values are determined only by their input values without observable side effects.

Even if you don’t normally write such functions, it is often useful to think about refactoring your code to create such functions. I find them particularly useful for business rule calculations such as calculating a discount rate based on properties of the customer and the purchase involved. Gary Bernhardt, in his Boundaries talk and related screencast, speaks about organizing applications into a functional core with an imperative shell wrapped around it.

Pure functions are relatively easy to test, but still worth talking about.

Coin Changer Kata

For this post, I’ll use the Coin Change code kata as an illustration. The original description of this kata is no longer online, but the basic idea is that you are given a set of coin denominations and an amount of money, and you need to return the fewest coins that add up to that amount.

Following a by-the-book TDD process, we’d start this kata by writing a very simple test and making it pass. I’ll use Ruby and RSpec for this post with a little Minitest thrown in at the end, but these ideas translate to any language or test framework.

Simplest Possible Test

For the first test, I’m just going test the degenerate case of returning no coins for an amount of 0 cents.

Skeleton
RSpec.describe "Coin Change Kata" do
it "returns no coins for 0 cents" do
end
end

This might seem like a really silly test but in order to write it, I have to make a number of design decisions. Tests are clients of the code they’re testing and are really good at identifying API problems early on. If your object is hard to set up or to use from the tests, there’s a good chance that the “real” clients of the API are going to have the same problem.

I progressively grow this first test as I make each design decision.

  • What message should I send in order to make change? I decided to call it exchange for now.
exchange message
RSpec.describe "Coin Change Kata" do
it "returns no coins for 0 cents" do
expect(exchange).to eq ???
end
end
  • Where should I send that message? That is, where should the method that implements the function live? Should it be a loose method, or should I create a class to host it? I decided to create a class.
Add a Subject
RSpec.describe "Coin Change Kata" do
subject(:some_object) { ??? }
it "returns no coins for 0 cents" do
expect(some_object.exchange).to eq ???
end
end
  • What should the class be called? CoinChanger. Often a class with a name ending in “-er” or “-or” is a design smell, and it might be in this case. But for now, it feels OK. I’ll definitely revisit this decision later.
CoinChanger class
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new(???) }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange).to eq ???
end
end

Note that I’ve changed the outermost describe block to be the class instead of a generic string. This isn’t strictly necessary. In many cases, the string is more descriptive and less coupled to implementation. But there is a very strong RSpec convention to use the name of the class being tested.

I don’t mind violating some conventions when it’s worth it, but in this case using the class in the outer describe block allows the use of RSpec features like the implicitly defined subject and the described_class method. David Chelimsky outlines good and bad uses of subject in this blog post.

I chose to use an explicitly-named subject here because I think it makes the code clearer; subject is a really generic name that doesn’t communicate much information.

I also chose to be explicit when creating the coin_changer. I used CoinChanger again rather than using described_class. This does introduce a bit of duplication, but it makes the test a bit easier to read and understand. If I end up creating a bunch of CoinChanger instances throughout the test file, I might switch over to using described_class because of the increased duplication. described_class is also very handy when you start using shared examples.

  • How do I create an instance of that class? Does it need any constructor parameters? Not yet. Just using new will be fine for now.
CoinChanger initialize
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange).to eq ???
end
end
  • What parameters should the exchange method take? Do I pass the amount into the class’ initialize method, or into the exchange method? I decided that a single CoinChanger could stick around and make change as many times as I like, so I should pass the amount into the exchange method. This has the happy side effect of making exchange a pure function, which is much easier to test.
Pass amount to exchange
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange(0)).to eq ???
end
end
  • What should the method return? I decided to return a Hash for now. I’m thinking that the keys of the hash will be coin denominations and the values will be the number of coins of that denomination. solution.
Output format
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange(0)).to eq({})
end
end
  • What about the requirement to take a set of coin denominations? I’ll worry about that later; I want my first test to be much simpler. Using TDD plus refactoring allows me to defer some design decisions until I have more information. In Lean Software Development: An Agile Toolkit, Mary and Tom Poppendieck talk about deferring commitment until the last responsible moment: the moment at which failing to make a decision eliminates an important alternative. That’s when we should make our design decisions.

With all of that design out of the way, it’s pretty easy to make the first test pass and it’s time for the next test.

Single Coin, Single Denomination

How about returning a single penny for one cent:

One Penny
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange(0)).to eq({})
end
it "returns a penny for 1 cent" do
expect(coin_changer.exchange(1)).to eq(1 => 1)
end
end

After making this test pass, we need to make a decision about which direction to go next. One option is to figure out how to return multiple coins of the same denomination; the other is to return coins of multiple denominations.

Multiple Coins, Single Denomination

I decided to do the former first and test for two pennies.

Two Pennies
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
expect(coin_changer.exchange(0)).to eq({})
end
it "returns a penny for 1 cent" do
expect(coin_changer.exchange(1)).to eq(1 => 1)
end
it "returns two pennies for 2 cents" do
expect(coin_changer.exchange(2)).to eq(1 => 2)
end
end

Removing Noise

At this point, the tests are starting to look very repetitive, and the important parts are obscured by the boiler-plate needed to actually call the code.

One thing we could do about this is extract the amounts and expected results to variables:

Extracting Variables
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
it "returns no coins for 0 cents" do
amount = 0
expected = {}
expect(coin_changer.exchange(amount)).to eq(expected)
end
it "returns a penny for 1 cent" do
amount = 1
expected = { 1 => 1 }
expect(coin_changer.exchange(amount)).to eq(expected)
end
it "returns two pennies for 2 cents" do
amount = 2
expected = { 1 => 2 }
expect(coin_changer.exchange(amount)).to eq(expected)
end
end

This helps a bit, but is still somewhat verbose. It does make the duplication more obvious. As a reader, though, I have to manually compare the duplicated lines to make sure they really are duplicated and that there isn’t some subtle, but important difference somewhere.

Table-Based Tests

A better option is to use a table-based test.

Table-Based Tests
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
[ # description amount expected
[ "no coins", 0, {} ],
[ "one penny", 1, { 1 => 1 } ],
[ "two pennies", 2, { 1 => 2 } ]
].each do |description, amount, expected|
it "returns #{description} for #{amount} cents" do
expect(coin_changer.exchange(amount)).to eq(expected)
end
end
end

Table-based tests are very useful when testing pure functions, because you can make a simple table of inputs and outputs and remove almost all of the noise and duplication.

Note that I could have written the table and its loop inside of a single it block, but that would make it harder to understand failures should they happen in the future. This way, I get a single it block with a good description for each row of the table.

With this table in place, I can easily add more rows to test additional cases until I’m sure my function is doing everything it needs to.

Tests Must Pay Their Own Way

I could add tests for three or four pennies. With the table in place, they’d be really easy to add, so what could it hurt?

That line of thinking is the downside to test tables or any other approach that makes it very easy and clean to add new tests. The more tests you have, the slower they run. Each new test might be a grain of sand but before you know it, you’ve got a beach. Each test must provide enough value to be worth the lifetime cost of maintaining it and running it over and over again.

Tests for more than two pennies are unlikely to teach me anything or to cause my code to change, so they’d just make my tests slower while adding no value. Now that I’m handling multiple coins of a single denomination, it’s time to support multiple denominations.

Multiple Coin Denominations

More Denominations
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
[ # description amount expected
[ "no coins", 0, {} ],
[ "one penny", 1, { 1 => 1 } ],
[ "two pennies", 2, { 1 => 2 } ],
[ "one nickel", 5, { 5 => 1 } ],
].each do |description, amount, expected|
it "returns #{description} for #{amount} cents" do
expect(coin_changer.exchange(amount)).to eq(expected)
end
end
end

Once again, I could continue adding tests for new denominations of coins (dimes, quarters, etc.), but those are unlikely to add any additional value. Depending on how I wrote the code to support nickels, though, I might need to add the dimes test to flesh out my design.

Dimes
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
[ # description amount expected
[ "no coins", 0, {} ],
[ "one penny", 1, { 1 => 1 } ],
[ "two pennies", 2, { 1 => 2 } ],
[ "one nickel", 5, { 5 => 1 } ],
[ "one dime", 10, { 10 => 1 } ],
].each do |description, amount, expected|
it "returns #{description} for #{amount} cents" do
expect(coin_changer.exchange(amount)).to eq(expected)
end
end
end

Some people would argue that the tests for quarters, 50-cent pieces, and dollar coins are necessary to force those denominations to exist since our list of coins is currently hard-coded. There is some merit to this argument, since there is nothing in the current test suite that will break when those denominations are missing.

However, those tests will really provide almost no value. We’ll write a different test later that will help cover this case.

Multiple Coins, Multiple Denominations

The next most valuable test to write is one where I need to return multiple coins of multiple denominations. Depending on how I wrote the code in response to the previous tests, this may not need additional work. But it’s still an important enough case to be worth testing.

Multiple Coins, Multiple Denominations
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
[ # description amount expected
[ "no coins", 0, {} ],
[ "one penny", 1, { 1 => 1 } ],
[ "two pennies", 2, { 1 => 2 } ],
[ "one nickel", 5, { 5 => 1 } ],
[ "one nickel and three pennies", 8, { 5 => 1, 1 => 3 } ],
[ "one dime", 10, { 10 => 1 } ],
].each do |description, amount, expected|
it "returns #{description} for #{amount} cents" do
expect(coin_changer.exchange(amount)).to eq(expected)
end
end
end

One Last Test

I’ll finish things up with a test for a combination of all of the coins. This is more like a higher-level acceptance or functional test that everything is working together. If a coin denomination is missing, this test will fail. If I’m not handling multiple coins of a denomination, this test will fail.

Acceptance Test
RSpec.describe CoinChanger do
subject(:coin_changer) { CoinChanger.new }
[ # description amount expected
[ "no coins", 0, {} ],
[ "one penny", 1, { 1 => 1 } ],
[ "two pennies", 2, { 1 => 2 } ],
[ "one nickel", 5, { 5 => 1 } ],
[ "one nickel and three pennies", 8, { 5 => 1, 1 => 3 } ],
[ "one dime", 10, { 10 => 1 } ],
[ "a mix of coins", 394, { 100 => 3, 50 => 1, 25 => 1, 10 => 1, 5 => 1, 1 => 4 } ]
].each do |description, amount, expected|
it "returns #{description} for #{amount} cents" do
expect(coin_changer.exchange(amount)).to eq(expected)
end
end
end

Deleting Tests

Now that I’ve driven out the code I need, I’m actually pretty comfortable with just the final “a mix of coins” test; the other tests were useful scaffolding to get me where I needed to be, but the final test checks all of the behavior I care about. Maybe I should delete those other tests.

On the other hand, I’ve got a pretty minimal set of focused tests that check each aspect of the method’s behavior. If any of those fail, I’ll get a targeted failure that tells me what I broke. The last test is more of an acceptance-level test.

Given that I know that I still need to support different sets of coin denominations, I’m going to keep the current tests in place.

At this point, I’m pretty confident in how my code handles American coins. We’ll look at supporting different coin denominations in the next post.

Minitest

If I were using Minitest instead of RSpec, the final test might look like this:

In Minitest
class CoinChangerTest < Minitest::Test
attr_reader :coin_changer
def setup
@coin_changer = CoinChanger.new
end
[ # description amount expected
[ "no_coins", 0, {} ],
[ "one_penny", 1, { 1 => 1 } ],
[ "two_pennies", 2, { 1 => 2 } ],
[ "one_nickel", 5, { 5 => 1 } ],
[ "one_nickel_and_three_pennies", 8, { 5 => 1, 1 => 3 } ],
[ "one_dime", 10, { 10 => 1 } ],
[ "a_mix_of_coins", 394, { 100 => 3, 50 => 1, 25 => 1, 10 => 1, 5 => 1, 1 => 4 } ]
].each do |description, amount, expected|
test_name = "test_returns_#{description}_for_#{amount}_cents"
define_method(test_name) do
assert_equal(expected, coin_changer.exchange(amount))
end
end
end

Note the use of define_method to dynamically create test methods. This is a bit tricker than using an RSpec it block, but not by much. RSpec is essentially doing the same thing under the hood.

I also modified the descriptions in the table so that I could more easily coerce them into proper method names.

Summary

  • Let the tests drive the design of your code, especially the public APIs of your objects. Testing pain is a good indication of pain that the clients will face.

  • Be explicit. Spell things out in the tests so that they serve as useful documentation. Be wary of how much of the “magic” of your testing framework you take advantage of.

  • Be clear. Try to eliminate unnecessary noise from your tests. For functions, table-based tests can be handy.

  • Make each test pay for itself. Tests cost time and money to run and to maintain. Don’t write tests that don’t add enough value to offset their cost. Or write them until you have the code in place, then delete them.

  • Defer decisions. Always try to defer design decisions until you are forced to make them. The longer you can wait, the more information you’ll have to make them.