When two things are orthogonal, it means that the angle between them is 90 degrees – at least in spaces with 3 dimensions or less. So when two vectors are orthogonal, they satisfy the property that there is no way to use the first one to express even the tiniest bit of what the other one expresses.
That is also how we should write our application code: methods and classes should be orthogonal to one another – i.e. no class should try to express what another class already expresses either in part or whole – therefore each class and each method should have only one responsibility, and thus one reason to change.
And test code is real code.
The corollary is that our tests should have only one single reponsibility as well.
That is why I hate tests that look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public void CanRecordPayment() { CreateMortgageDeed(1, new DateTime(2003, 3, 11)); CreateMortgageDeed(2, new DateTime(2003, 6, 11)); CreateMortgageDeed(3, new DateTime(2003, 3, 11)); CreateMortgageDeed(4, new DateTime(2003, 3, 11)); CreateMortgageDeed(5, new DateTime(2003, 9, 11)); var mortgageDeeds = new DueTermsFinder(new MortgageDeedRepository()) .FindDueTermsBefore(new DateTime(2003, 4, 1)); Assert.AreEqual(3, mortgageDeeds.Count); mortgageDeeds = mortgageDeeds.OrderBy(m => m.CaseNo); Assert.AreEqual(1, mortgageDeeds[0].CaseNo); Assert.AreEqual(3, mortgageDeeds[1].CaseNo); Assert.AreEqual(4, mortgageDeeds[2].CaseNo); var result = new TermDebitRecorder() .CreateAndRecordTermDebits(mortgageDeeds); Assert.AreEqual(3, result.RecordedTermDebits.Count); Assert.AreEqual(new DateTime(2003, 3, 11), result.RecordedTermDebits[0].TermDate); Assert.AreEqual(new DateTime(2003, 3, 11), result.RecordedTermDebits[1].TermDate); Assert.AreEqual(new DateTime(2003, 3, 11), result.RecordedTermDebits[2].TermDate); // .... etc! } |
Notice how this test is actually fairly decently structured – at least that’s what it initiallt looks like… but it actually tests a lot of things: it checks that the output of the DueTermsFinder is what it expects, testing the MortgageDeedRepository indirectly as well – and then it goes on to test the TermDebitRecorder … sigh!
If (when!) one of these classes changes at some point, because the requirements have changed or whatever, the test will break for no good reason. The test should break because you have introduced a bug, not because you made a change in some related functionality.
That is why I usually follow the pattern of AAA: Arrange, Act, Assert. Each test should be divided into discrete steps corresponding to 1) Arranging some data, 2) Triggering a computation or some state change, 3) Asserting that the outcome was what we expected. And if I am feeling idealistic that day, I also follow the principle of putting only one assertion at the end of each test.
I try to never do AAAAA (Arrance, Act, Assert, Act Assert) or AAAAAA, or AAAAAAA which is even worse.
Every test should have only one reason to break.
One thought on “Respect your test code #3: Make your tests orthogonal”