- Published on
TDD gears and equivalent partitions
- Authors
- Name
- Yair Mark
- @yairmark
At a conference I went to earlier this year I won a 3 day voucher to a course on TDD. This course is facilitated by Chillisoft through their DevFluence training school. Chillisoft is very well known and respected for their strong TTD capabilities. On winning this course I was very eager to get into it despite the fact that I have been TDDing my code in various shapes and forms throughout my career. After day 1 my expectations were very much exceeded. The course so far has introduced some ideas about TDD that I have not encountered before or fully explored.
Different Gears - Low and Medium
The course introduced me to the idea of gears. There are 4 types:
- Low gear
- Medium gear
- High gear
- Reverse gear
Day 1 focused mainly on low gear and a bit on medium gear. The idea is you use these gears in different situations and to approach different types of problems. Low gear is where you are getting a feel for what a code problem is about. Your first test will likely have a name that will not make sense, the instructor even suggested calling the test something like learning()
until you figure it out. The initial part of low gear is for you to start building confidence in your tests so you start simple.
For example say I am testing a class that simply sums 2 numbers sum(num1: Int, num2: Int)
. Assuming I am doing this pure TDD I need to start with a breaking test:
@Test
fun learning() {
val result = Adder.sum(1,2)
assertEquals(3, result)
}
This will initially fail as we do not even have an Adder
class which is perfectly valid for our first "red" or failing case. So we create it and our errors disappear. This class will be coded initially to return the value 3
irrespective of the input:
class Adder {
fun sum(num1: Int, num2: Int): Int {
return 3
}
}
This is a contrived example but it demonstrates that we start simply and write the minimal amount of code to make the test pass. Despite the fact that this is a simple test -in terms of what it tests- it still starts to give us confidence in the test harness we are building up. We can now rename our test to be something more specific:
@Test
fun sum_givenOneAndTwo_ReturnsThree() {
val result = Adder.sum(1,2)
assertEquals(3, result)
}
This approach to naming is based on Roy Osherove's approach that can be found here. The core idea is that if it breaks you can immediately understand why it is failing off of the test name alone. But a key point is that you should align your test naming convention with what is used in a project for other test otherwise it can become chaos as people will become confused with all the different ways tests are named especially if they fail when code breaks.
The next step is to write tests that add another pair of numbers which will break as we currently hard code the result. This process helps us understand, test by test and each red-green cycle how the code we are trying to solve for should likely work. This low gear approach will initially result in many many small tests each helping us understand what our code should be doing and how it should be doing it. This helps us start to more deeply understand our business problem.
Armed with this knowledge we can start moving into medium gear. This based on day 1 so far is the process of collapsing a bunch of tests into a more focused test which targets specific partitions and boundaries - I will discuss these concepts more deeply in the following section. Another rule of thumb for collapsing multiple tests into one is can we name this new single test in such a way that it makes sense and we understand what the test does? If the answer is no then we likely have not properly understood the data that forms a problem and should reverse gears and switch back to low gear. We then start to write more exploratory tests to better under stand how to slice and dice the data. This will then hopefully better guide us when we switch back to medium gear to start trying to collapse multiple similar tests into one test.
The day today focused mainly on understanding what low gear is about and better getting a feel for some of the core TDD mechanics. The following days will likely delve more into what medium gear is and give clarity on what high gear is about.
Equivalence Partitions and Boundaries
Equivalence partitions are buckets into which the components/data of a problem fit. For example say I have something that works on integers that can be positive, zero and negative this can be split into the following:
- Equivalence partition 1: Negative numbers
- Boundary: Zero
- Equivalence partition 2: Positive numbers
Based on the simple example above the data for this contrived code works on 2 buckets of data: positive data and negative data and one boundary which is 0. These buckets are a very good and concrete guide for signalling to ourselves when we have sufficient tests. If we have a test suite that covers each bucket and the boundary we likely have our code covered.
The classic FizzBuzz Kata can be approached using this. The basic kata problem is given a number as input return:
- Fizz if the number is a multiple of 3
- Buzz if the number is a multiple of 5
- FizzBuzz if the number is a multiple of 3 and 5
- The number if it does not satisfy any of the above conditions
Example inputs and outputs would be:
Input | Output |
---|---|
1 | 1 |
2 | 2 |
3 | Fizz |
4 | 4 |
5 | Buzz |
15 | FizzBuzz |
25 | Buzz |
27 | Fizz |
30 | FizzBuzz |
The above demonstrates what our algorithm needs to satisfy in order for its business rules to have been implemented properly. Based on these required rules and examples there are 4 buckets:
- Numbers divisible by 3 should return Fizz
- Numbers divisible by 5 should return Buzz
- Numbers divisible by 15 should return FizzBuzz
- Numbers that are not divisible by 3, 5 or 15 should simply be returned
Based on this example not all problems have clear or linear boundaries. Boundaries can also be seen as a partition which makes thinking about none-linear boundaries easier. The buckets we identified above make it more clear we likely need at least 4 suites of tests to get optimal coverage of the fizz buzz problem. By writing low gear tests for this where we start by writing a test to target individual numbers and outcomes we can get a feel for the code until we are confident it is working and collapse the code into fewer tests as we switch to medium gear and target specifically the equivalence partitions we have identified. This approach results in us writing minimal, terse, focused and fluff free production code as we write as little as possible to simply make the tests pass.