Red Green Repeat Adventures of a Spec Driven Junkie

TDD In-depth Divide Part 3

In my previous article, TDD In-depth Divide Part 2, we have the code passing tests that accept a numerator and denominator as arguments to the method and divide by 1 tests. The divide by 2 tests just started after covering exceptions such as method arguments.

Let’s write more tests to divide by 2 and move on!

William Michael Harnett - The Banker's Table

Requirements

Before starting, if you would like to follow along with me, I set up a vagrant configuration that you can download and get started easily:

I like to have a consistent environment that is working. The environment I will be using is from a Vagrant box. The set up files are here

Installing Vagrant and Virtualbox information can be found at:

With the file downloaded, run: $ vagrant up, which will create a working environment. Connect to the environment using: $ vagrant ssh.

All of the commands and code will be working from within the vagrant environment.

I will continue exactly where the last article ends, which code is available here

and the specific commit is here

Test: Divide 2 by 2

After dividing 1 by 2, the next logical test to write is: divide 2 by 2!

Let’s follow the same form as the divide 1 by 2 test:

  it 'by 2 properly' do
	expect(divide(1,2)).to eq(0)
	expect(divide(2,2)).to eq(1)
  end

Which, when running the test results in:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
....F

Failures:

  1) divides by 2 properly
	 Failure/Error: expect(divide(2,2)).to eq(1)

	   expected: 1
		    got: 0

	   (compared using ==)
	 # ./spec/divide_spec.rb:31:in `block (2 levels) in <top (required)>'

Finished in 0.01627 seconds (files took 0.10496 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:29 # divides by 2 properly

Yup, considering the way divide is implemented now, there is no way this test would pass. Let’s make it pass.

Code: Divide 2 by 2

The number of tests are coming to a point where the code would start to get complicated.

I will take a bit of a quantum leap and re-use an implementation from the original article. ;-) I am at a point with the tests that a basic division algorithm will help a lot.

If you would like, try implementing the method on your own. The rest of the article will be here when you are done.

def divide(numerator, denominator) counter = 0 while numerator - denominator >= 0 numerator -= denominator counter +=1 end counter end

Running tests:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
.....

Finished in 0.00756 seconds (files took 0.10328 seconds to load)
5 examples, 0 failures

Great, this will probably cover the next set of “divide by 2” tests, right?

Test: Divide anything by 2

Now that 2 divides by 2, let’s write another test to cover almost any other situation. I will follow a similar pattern as before when dividing 1 by anything.

  it 'by 2 properly' do
	expect(divide(1,2)).to eq(0)
	expect(divide(2,2)).to eq(1)

	numerator = rand(1..1_000_000)
	expect(divide(numerator, 2)).to eq(numerator/2)
  end

Let’s run the test to see if our hypothesis that the code is robust enough to cover this:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
.....

Finished in 0.02936 seconds (files took 0.10277 seconds to load)
5 examples, 0 failures

Great, since the code is robust enough to divide anything by 2, this test passes so there is no need for new code.

Test: Divide anything by anything

As the code is robust enough to divide anything by 2, instead of writing a set of tests to divide by 3, let’s write a series of tests where anything can be divided by anything:

  it 'any integers properly' do
	expect(divide(2,3)).to eq(0)
	expect(divide(10,10)).to eq(1)
	expect(divide(14,7)).to eq(2)
	expect(divide(300, 100)).to eq(3)

	numerator = rand(1..1_000_000)
	denominator = rand(1..1_000_000)
	expect(divide(numerator, denominator)).to eq(numerator/denominator)
  end

Checking if this is the case:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
......

Finished in 0.06378 seconds (files took 0.10428 seconds to load)
6 examples, 0 failures

Note: I mixed a few fixed cases, where the numerator, denominator, and result are known, with a generated example.

And the generated example actually used / to calculate the result:

expect(divide(numerator, denominator)).to eq(numerator/denominator)

This might be considered cheating, according to the original problem: write divide without divide, but this is not writing divide with divide.

It is merely testing the divide method with /.

In the tests, I want to use any techniques available to help create tests for the method, even using ones “not allowed”. The method still does not use /, just the test.

Breadth & Depth Tests

The fixed cases provides depth, if there is a focus to test certain cases, write the test for it.

The random cases provide breadth, as values are chosen randomly from a range, each test run will test the function on a wide range of any possible values.

As these all pass, there is no need for additional code. I keep these here as a way to warn of any changes that break the method definition.

Optional: Refactor

As the number of tests cover the basic case of dividing anything by anything, there’s enough tests to completely rework the current code for the divide method.

Can there be a better implementation for the divide method? It’s easy now because the tests only assert that for certain inputs give the expected output.

However the internal method changes, it won’t matter to the tests. For example, if cheated and implemented the divide method as:

def divide(numerator, denominator)
  numerator / denominator
end

All the tests will still pass.

While implementing the method as:

def divide(numerator, denominator)
  numerator + denominator
end

Will cause all the tests to fail. So you know right away.

This is part of TDD I love. Can I get the code part to be:

  • Better
  • Shorter
  • Faster
  • Clearer

With each change, I just run tests again to see how I am progressing.

Try this out: currently, the method uses a while loop, can you think of a better way of implementing this? Something that’s shorter, faster, or clearer? Or conversely, more complicated, lower-level, or just plain different?

Edge Cases

With basic division working, let’s move onto edge cases. The first one: string arguments.

Test: String Arguments

In general, string arguments are not bad, but in the divide method just written, how shall string arguments be handled?

Let’s start with a naive test:

describe 'bad arguments' do
  it 'strings' do
	expect(divide("1", "1")).to eq("1")
  end
end
vagrant@ubuntu-xenial:~/tdd_series$ rspec
......F

Failures:

  1) bad arguments strings
	 Failure/Error: expect(divide("1", "1")).to eq("1")

	 NoMethodError:
	   undefined method `-' for "1":String
	   Did you mean?  -@
	 # ./divide.rb:3:in `divide'
	 # ./spec/divide_spec.rb:47:in `block (2 levels) in <top (required)>'

Finished in 0.15567 seconds (files took 21 minutes 30 seconds to load)
7 examples, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:46 # bad arguments strings

This test fails, but not in an expected way, which starts a whole new conversation:

How shall the divide method handle string arguments?

In this case, a simple: numerator.to_i would be the start of the solution. This brings up another question:

If the numerator or denominator is a string, what shall the result be? An integer? An integer converted into string?

This becomes trickier to answer. One response could be: return the result in the same type. String in, string out.

Which can lead to another question:

What if only one of the arguments is a string?

Hmm… return string if one of the argument types are string? Return integer if one of the argument types are integer? Always return integer?

These are all valid answers, but require decisions to be made. There are downstream effects.

Ultimately, whatever the decision, it’s important to have tests that embody these decisions. These kinds of design decisions are not always obvious from code, so having tests that describe these decisions will:

  1. Make it easier to implement the decision
  2. Codify the decision one at a time
  3. Describe the decision
  4. Viewable for the next person working on this
  5. Keeps these decisions out of implementers head
  6. Sets up requirements for the decision

Decision time

In such situations where there can be many right answers and I start going down a path of questions such as these I ask:

Is there a reference I can use as a guide?

In this case, yes! The original problem is to write divide without divide, so I will take that to mean: emulate Ruby’s built-in divide operator / as much as possible.

How does / work with strings? Let’s test it out in IRB:

2.4.5 :001 > "1"/"1"
NoMethodError: undefined method `/' for "1":String
        from (irb):1
        from /home/vagrant/.rvm/rubies/ruby-2.4.5/bin/irb:11:in `<main>'
2.4.5 :002 > 

So, it looks like / on a string returns NoMethodError, which is a bit unsatisfactory response for a user using this divide method. Any user would just see that this method cannot handle strings and it would be a bug.

When in fact, this class of arguments are not supported, as the original / method doesn’t.

So, let’s return an error that is descriptive, such as: ArgumentError, so it describes the nature of the error.

Now that I have taken that decision, let’s re-write our test:

describe 'bad arguments' do
  it 'strings' do
	expect { divide("1", "1") }.to raise_error ArgumentError
  end
end

and result is:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
......F

Failures:

  1) bad arguments strings
	 Failure/Error: expect { divide("1", "1") }.to raise_error ArgumentError

	   expected ArgumentError, got #<NoMethodError: undefined method `-' for "1":String
	   Did you mean?  -@> with backtrace:
		 # ./divide.rb:4:in `divide'
		 # ./spec/divide_spec.rb:47:in `block (3 levels) in <top (required)>'
		 # ./spec/divide_spec.rb:47:in `block (2 levels) in <top (required)>'
	 # ./spec/divide_spec.rb:47:in `block (2 levels) in <top (required)>'

Finished in 0.06554 seconds (files took 0.10897 seconds to load)
7 examples, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:46 # bad arguments strings

While technically, the method can be left as is. Why? It raises an error: NoMethodError, which is a form of error and an exception is raised, just a different one.

In this case, I would prefer to have the error raise ArgumentError because the nature of the error is conveyed, even to an internal user of the method.

This is good practice, as starts to set up the right infrastructure to handle edge cases and also provide information to method users on how to resolve the problem themselves, rather than bother you by saying: “Oh, your method is broken.”

In this case, you can say: “That case has been considered and the decision is that it is an error.” Specifically, an ArgumentError.

Now, let’s get this test passing.

Code: String Arguments

Again, what’s the minimum amount of code to pass the test? Let’s start with:

def divide(numerator, denominator)
  raise ArgumentError if numerator.is_a?(String)
  counter = 0
  while numerator - denominator >= 0
	numerator -= denominator
	counter += 1
  end

  counter
end

Which results in:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
.......

Finished in 0.02742 seconds (files took 0.14542 seconds to load)
7 examples, 0 failures

I always prefer to take small steps with tests and code.

Test: String Denominator

As the code was only checking the numerator, let’s write a test that would break that code.

For this test, I’m using the knowledge of the implementation to create a test that intentionally fails. This way, the tests cover more cases.

describe 'bad arguments' do
  it 'strings' do
	expect { divide("1", "1") }.to raise_error ArgumentError
	expect { divide(1, "1") }.to raise_error ArgumentError
  end
end

Running tests to make sure there’s a failure:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
......F

Failures:

  1) bad arguments strings
	 Failure/Error: expect { divide(1, "1") }.to raise_error ArgumentError

	   expected ArgumentError, got #<TypeError: String can't be coerced into Integer> with backtrace:
		 # ./divide.rb:5:in `-'
		 # ./divide.rb:5:in `divide'
		 # ./spec/divide_spec.rb:48:in `block (3 levels) in <top (required)>'
		 # ./spec/divide_spec.rb:48:in `block (2 levels) in <top (required)>'
	 # ./spec/divide_spec.rb:48:in `block (2 levels) in <top (required)>'

Finished in 0.07891 seconds (files took 0.10946 seconds to load)
7 examples, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:46 # bad arguments strings

This test is intentionally looking inside the code, but creating a scenario that would exist regardless of the code.

Code: String Denominator

Let’s take a simple approach and just add onto the last piece of code:

def divide(numerator, denominator)
  raise ArgumentError if numerator.is_a?(String) || denominator.is_a?(String)

  counter = 0
  while numerator - denominator >= 0
	numerator -= denominator
	counter += 1
  end

  counter
end

Test: String Numerator

Let’s add one more test to make sure one scenario didn’t slip through: where the denominator is an integer but the numerator is a string.

This case was basically caught by the first test, but for completeness, I am adding this test.

describe 'bad arguments' do
  it 'strings' do
	expect { divide("1", "1") }.to raise_error ArgumentError
	expect { divide(1, "1") }.to raise_error ArgumentError
	expect { divide("1", 1) }.to raise_error ArgumentError
  end
end

Without surprise:

vagrant@ubuntu-xenial:~/tdd_series$ rspec
.......

Finished in 0.05107 seconds (files took 0.12609 seconds to load)
7 examples, 0 failures

A main reason for having a complete set of tests, even when they won’t break: all known edge cases are codified.

For me, this allows greater freedom in refactoring, so I just know the tests will cover all known situations. I can go nuts with refactoring the method.

It’s kind of addicting. :-)

Exercise

At this point, I feel there can be more scenarios to consider:

  • Zeroes
  • Negative arguments

Instead of me writing them out, feel free to try writing the tests first, then the code. Contact me with your result, or if you want guidance and/or feedback.

Conclusion

The divide method is dividing without divide with tests covering main cases and an edge case of strings.

Even though this is a simple piece of code, there were a lot of tests. Most of the tests created in this article were not around division itself, but were for edge cases such as:

  • What if the method does not exist?
  • Does method accept two arguments?
  • What if strings were passed in?

Additional edge cases such as zeroes and negative numbers would be the next set of tests I would cover.

At the same time, when there is a situation where the right way of doing something (i.e. handling string arguments) is not simple, use a test to codify the decision.

This is the main reason I prefer to not only have tests for my code, but to test first, then code.