One thing, that I find really annoying, is that somehow it seems to be accepted that test code creates instances of this and that like crazy! In a system I am currently maintaining with about 1MLOC where ~40% is test code, I often come across test fixtures with something like 20 individual tests, and each and every one of them creates instances of maybe 5 different entities, builds an object graph, runs some code to be tested, and does some assertions on the result.
This is a super example on how to write brittle and rigid tests, because what happens if the signature of one of the ctors changes? Or the semantics of the object graph? Or [insert way too many ways for the test to break for the wrong reasons here…].
When I come across these tests, I usually factor out the creation of all but the most simple objects behind methods with meaningful names. This has two advantages: 1) It’s more DRY because they can be re-used, and 2) It serves as brilliant documentation. Consider this rather simple assertion that requires a few objects to be in place:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[Test] public void MortgageDeedKnowsItsActualPrincipalBeforeAmortizationHasBegun() { var deed = new MortgageDeed(); deed.Principal = 100000; deed.Amortization.FirstTerm = new DateTime(2003, 3, 11); deed.Amortization.AddAmortizationStrategyElement(new AnnuityAmortizationStrategyElement { FirstTerm = 1, TermsPerYear = 4, PeriodicPayment = 5000, }); deed.Amortization.AddInterestStrategyElement(new FixedRateInterestStrategyElement { FirstTerm = 1, Rate = 0.05, }); Assert.AreEqual(new DateTime(2003, 3, 11), deed.ActualPrincipalDate); } |
compared to this:
1 2 3 4 5 6 7 8 9 10 11 12 |
[Test] public void MortgageDeedKnowsItsActualPrincipalDateBeforeAmortizationHasBegun() { var deed = NewStandardMortgageDeedBeforeAmortization(new DateTime(2003, 3, 11), 100000, 5000); Assert.AreEqual(new DateTime(2003, 3, 11), deed.ActualPrincipalDate); } MortgageDeed NewStandardMortgageDeedBeforeAmortization(DateTime firstTerm, decimal principal, decimal periodicPayment) { //... } |
MUCH more clear! The factory method acts as brilliant documentation on which aspects of the test are relevant to the outcome – it is clear, that a mortgage deed which has not yet begun its amortization must report its first term date as the actual principal date.
Go on to test another property of the mortgage deed before amortization:
1 2 3 4 5 6 7 |
[Test] public void MortgageDeedKnowsItsActualPrincipalBeforeAmortizationHasBegun() { var deed = NewStandardMortgageDeedBeforeAmortization(new DateTime(2003, 3, 11), 100000); Assert.AreEqual(100000, deed.ActualPrincipal); } |
– and we have already saved us from writing 50 lines of brittle rigid test code. Keep factoring out common stuff, so that the ctor of the mortage deed is still only called in one place… e.g. create methods 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 30 31 32 33 34 35 36 37 |
MortgageDeed NewStandardMortgageDeedWithTransactions( DateTime firstTerm, decimal principal, decimal periodicPayment, params Transaction [] transactions) { var deed = NewStandardMortgageDeedBeforeAmortization(firstTerm, principal, periodicPayment); Array.ForEach(transactions, t => deed.AddTransaction(t)); } Transaction NewPayment(DateTime paymentDate, decimal amount) { var payment new PaymentTransaction { Amount = amount, PaymentDate = paymentDate, ValueDate = paymentDate, }; payment.Record(); return payment; } Transaction NewTermDebit(DateTime termDate, decimal interest, decimal installment) { var termDebit = new TermDebitTransaction { Interest = interest, Installment = installment, TermDate = termDate, } termDebit.Record(); return termDebit; } |
which allows me to write cute easy-to-understand tests 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 |
[Test] public void ActualPrincipalDateIsChangedWhenPaymentIsMade() { var deed = NewStandardMortgageDeedWithTransactions( new DateTime(2003, 3, 11), 100000, NewPayment(new DateTime(2003, 6, 11), 5000)); Assert.AreEqual(new DateTime(2003, 6, 11), deed.ActualPrincipalDate); Assert.AreEqual(new DateTime(2003, 3, 11), deed.AmortizedPrincipalDate); } [Test] public void AmortizedPrincipalDateIsChangedWhenTermDebitIsMade() { var deed = NewStandardMortgageDeedWithTransactions( new DateTime(2003, 3, 11), 100000, NewTermDebit(new DateTime(2003, 6, 11), 4000, 1000)); Assert.AreEqual(new DateTime(2003, 3, 11), deed.ActualPrincipalDate); Assert.AreEqual(new DateTime(2003, 6, 11), deed.AmortizedPrincipalDate); } |
This is also a good example on how to avoid writing // bla bla comments all over the place – it’s just not necessary when the methods have sufficiently well-describing names.
One thought on “Respect your test code #1: Hide object instantiation behind methods with meaningful names”