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!
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:
Which, when running the test results in:
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 endRunning tests:
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.
Let’s run the test to see if our hypothesis that the code is robust enough to cover this:
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:
Checking if this is the case:
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:
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:
All the tests will still pass.
While implementing the method as:
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:
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:
- Make it easier to implement the decision
- Codify the decision one at a time
- Describe the decision
- Viewable for the next person working on this
- Keeps these decisions out of implementers head
- 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:
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:
and result is:
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:
Which results in:
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.
Running tests to make sure there’s a failure:
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:
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.
Without surprise:
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.