So,
In my continuing saltiness over the demands the *DD’ers are making on our community, I realized yet another angle for scrutinizing their claims, which I’ll present with an example. But first – my thesis for those who won’t read any farther without it: Engaging in Peer Review is a superior use of a developers’ time to writing tests, and ought to replace all but integration tests- just because we can spend more time with our hands on the keyboards writing around our actual product (production code) doesn’t mean we get a better product by doing so. Here’s the example:
Let’s say I’ve written some code that transforms a list of (date,value) tuples into a crosstab-table with months in columns, and years in rows (and a year-end aggregate column as well).
If you know me, you know that I didn’t write tests for this first – I pondered the approach to use, then I coded up the approach. I evaluated it on a couple of sets of data, including some edge cases. When I passed those cases, I stepped back and looked at my code – does it read like it does what I intended to do ? Yes ! I showed the code to a couple of other team members – got nods of approval – they didn’t raise any edge case questions I wasn’t already handling. I was done, right ? In record time ? Well, not if I have to write tests, I’m not.
At this point my objection to writing tests is that I’ve already proved that the code meets the standard me and my coworkers have thought of, so I feel like I don’t want to do the pessimistic monkey-work of writing low-level assertions on what data is actually in row 2, column 1, given a set of inputs – my console-based way of verifying results showed me that writing these tests up will not reveal more.
But not content with that first answer (it could be laziness masked by rationalization), I decided to antagonize my answer with these questions:
- But don’t I want to find problems in my design ?
- But don’t I want to regression-proof my design ?
- But don’t I want to preserve the value of the inline testing that I did ?
And my responses, in order are:
- But don’t I want to find problems in my design ? Yes, I would love to, but the mechanics of writing up tests will not reveal them. The brain-work of coming up with edge cases to test – this has been done – and may continue to be done – and the code has been adjusted. The proof of having dealt with the edge cases lies not in the tests, but in the code that safely handles those cases. Writing those edge cases up as tests is monkey-work – the code *is* the proof. Also – I asked some of the brightest in my company to challenge my design.. Peer review is what all of Science is based on – that’s a high enough standard for me. I trust my coworkers. And I really want them or me to find a flaw in my design, but for the time being, it appears one is not discoverable. To believe that tests will find a problem before a brain finds a problem is to confuse cause and effect.
- But don’t I want to regression-proof my design ? My code to create the crosstab lies in a function, the internals of which depend only on core library functions of the language I’m using. The code is readably, if not provably, correct. Must I double the amount of time I’m spending on this feature, only to protect the ass of a hypothetical future person who violates the Rule – Do Not Change It If You Dont Fully Understand It ? Sure, tests could help explain what it fully does (and rule out accidentally correct behaviors of the code), but that person has no out, as far as I’m concerned, whether there are tests or not, for not fully understanding what they’re about to change. In fact, coming up with the tests themselves is more likely to educate them on what it does than my doing it for them.
- But don’t I want to preserve the value of the inline testing that I did ? Ahh, yes. Cory Haines has mentioned this angle to me. I think it has some validity to say that I’m Bad for throwing out the console evals that I did. But to reify those into Unit tests just may not be worth it. I visually verified a whole lot- to code all those up as assertions- tedious and time-consuming !! When the problem is already solved per the standard of Peer Review, not too appealing. I’m comfortable the only thing that will break this particular code is – core library changes, and incautious changes by other developers. Perhaps the time to write the test is best bundled up with the task “Upgrade the core library and be sure it all still works”. Perhaps integration-level tests will be sufficient to catch any breakage.. I can’t be sure, but I can be sure there are other things I can be doing with my next 60 minutes that have greater value – like explaining to a QA how they can write an integration test against this bad boy.
In closing, I find there are many valid reasons not to write unit tests, and particularly to not practice TDD. I of course believe there is a place for them, and particularly integration tests, but to say that every elegant code solution needs an ugly verification unit test is to forget that tests are only as powerful as the Brains that write them. If you pass 100% of brain tests, that may be good enough.
Appendix: Here’s the code. What edge cases would you suggest I test ? I’ll send a dollar via US Postal Service to the first person who proposes an angle I haven’t considered (you’ll have to trust me on this, of course
)
The goal is transforming a list of pairs of (date, amount) values (which are fields of a class called Return) such as:
2006-11-01 .76%
2006-12-01 1.47%
Into a table like this:
Year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Year
2011 -0.41% -0.08% -0.10% 1.89% 0.21% 1.22% 1.27% -2.46% N/A
1.49%
2010 -0.11% 0.80% 2.85% 1.40% -1.03% -0.73% 1.93% 1.59% 2.82% 0.86% -0.75% 1.12% 11.19%
2009 -1.71% -4.64% 1.00% 4.77% 1.14% 1.40% 3.58% 2.79% 1.62% -0.45% 0.48% 1.54% 11.78%
2008 -3.12% 1.75% -1.92% 0.30% -0.90% -3.80% 0.50% -0.80% -7.80% -8.90% -0.80% 3.23% -20.72%
2007 1.40% 0.68% 0.67% 1.32% 1.85% -1.38% 0.00% 0.00% 1.57% 1.59% -2.28% -1.24% 4.16%
2006
0.76% 1.47% 2.24%
The code that does it is:
List returns = //get some Return objects...
var query = returns
.GroupBy( c => c.periodStart.Year )
.Select( g => new {
Year=g.Key,
Jan=g.Where( c => c.periodStart.Month == 1).FirstOrDefault(),
Feb=g.Where( c => c.periodStart.Month == 2).FirstOrDefault(),
Mar=g.Where( c => c.periodStart.Month == 3).FirstOrDefault(),
Apr=g.Where( c => c.periodStart.Month == 4).FirstOrDefault(),
May=g.Where( c => c.periodStart.Month == 5).FirstOrDefault(),
Jun=g.Where( c => c.periodStart.Month == 6).FirstOrDefault(),
Jul=g.Where( c => c.periodStart.Month == 7).FirstOrDefault(),
Aug=g.Where( c => c.periodStart.Month == 8).FirstOrDefault(),
Sep=g.Where( c => c.periodStart.Month == 9).FirstOrDefault(),
Oct=g.Where( c => c.periodStart.Month == 10).FirstOrDefault(),
Nov=g.Where( c => c.periodStart.Month == 11).FirstOrDefault(),
Dec=g.Where( c => c.periodStart.Month == 12).FirstOrDefault(),
Annual=g.Aggregate(1.0, (running, one) => running * (one.amount + 1.0) )
}).OrderByDescending(g => g.Year);