If we really wanted to demonstrate that the Deck code was using the random number sequence, we could create a second Random object with a different seed. Then we would write more assertions to prove that the cards came off in x, y, and z order.
Why might I write a second test like that? If you think about it, it would be possible to code the Deck class so that it explicitly returned cards in the same sequence as the one generated by the Random object. Ludicrous!
Think about what we’re doing with test-driven development. The primary goal of TDD is to drive the development of your system through the specification of unit tests. And while a secondary goal is to alert you when you or another programmer creates a defect with new code, TDD is not a security system! It’s not intended to be a tool that allows you to remain ignorant of what’s going on in your system. It’s not a tool for preventing code sabotage.
If you try to use TDD to protect you from every last possible thing that could go wrong in your system, you’ll fail. First, it’s virtually impossible to test everything. Trying to do so will keep you in the business of writing tests, instead of in the business of shipping code. Second, if you’re compelled to write tests to protect you from the nefariousness of someone deliberately sabotaging your code, you have other, far more serious problems. TDD isn’t the way to solve them!
Instead, your tests should be sufficient enough to give you confidence. You want each unit test to document some behavior, and you want each to demonstrate that, in general, the expected behavior occurs given that specific set of circumstances. You may need to test a handful of different scenarios, including those that generate errors. In the next installment of this series, I’ll discuss how to write tests for error conditions.
Over time, you’ll learn how many tests to write based on confidence. A defect will diminish that confidence. Use that negative feedback as a lesson to increase your level of testing accordingly.