I care about my craft and want to improve. I learn from the existing body of knowledge, practice the patterns and gradually gain proficiency in them. Through my journey I pay attention to the hits & misses of each pattern, ultimately developing a sense of when it's a good idea to use one or not.
Test-driven development is a pattern we use daily at Dynamic.Tech. In my experience as a consultant helping companies improve the quality of their code, I found many teams had given up before gaining proficiency in this practice. They were missing out on having:
I used to work at, and still collaborate with, a company which inherited practices from Uncle Bob. He's one of the main proponents of TDD & other patterns.TDD requires proficiency in other patterns in order to be effective.
You've probably heard about the SOLID principles. Applying them to our production & test code has given us the benefits of TDD I previously described. The guidelines below stem from the principles.
Using the FizzBuzz kata as an example:
Write a program that prints the numbers from 1 to 100. ...but for multiples of three print "Fizz" instead of the number. ...and for the multiples of five print "Buzz". ...For numbers which are multiples of both three and five print "FizzBuzz". Example Output: 1, 2, Fizz, 4, Buzz, [...], 13, 14, FizzBuzz, ...
We don't care *how* we implement the solution. We care about the following behavior:
So that's what we aim to cover in our tests.
This is where you get design feedback. How would you like this unit to look like to other elements in your system?
expect(FizzBuzz.generate(3)).to eq([1, 2, "Fizz"])
The code example above intended to verify the first behavior:
- Multiples of 3 print "Fizz"
Yet, by comparing against the entire list, we're also covering:
- Prints every other number
The problem is evident when we add more test cases to verify multiples of three:
expect(FizzBuzz.generate(9)). to eq([1, 2, "Fizz", 4, 5, "Fizz", 7, 8, "Fizz"])
The previous assertion now overlaps with yet another behavior:
- Multiples of 5 print "Buzz"
When we implement "Buzz" then the code above would break ("Expected 5, got Buzz"). Having one reason to change will help our test case resist even if other behaviors are added, or break.
list = FizzBuzz.generate(9) [2, 5, 8].each do |n| expect(list[n]).to eq("Fizz") end
Help future developers understand the unit's behavior. When something breaks, your test case should explain why the expectations were so.
it "returns 'Fizz' on multiples of three" do list = FizzBuzz.generate(100) [3, 6, 9, 15, 48, 72, 99].each do |n| expect(list[n-1]).to eq("Fizz"), n end end
That last `n` parameter in the assertion complements the failure message (in our testing framework). When the test fails it'll show a helpful value:
Failure on line X: "Expected Fizz, but got FizzBuzz - 15"