I wasn’t sure how I’d like to begin this blog post. Instead of writing a long and boring introduction that drives my point, I’ll just dump all that boiled inside me for quite a while. Sarcasm and bitterness ahead!
I’ll start from the end: it wasn’t until a former colleague of mine, Gil Zilberfeld, wrote his post about how in our industry we shifted from choosing individuals and interactions over tools and processes (or whatever the Church of Agile calls it) to having an almost a knee-jerk reaction to someone doing horrible things to our vision of a perfect code.
Think about it, how many times have you started a new job or met a new client, and once you’ve managed to get their code onto your machine, you shrieked in horror and facepalmed over some things you people wouldn’t believe? A SomethingManager class that takes in 12 dependencies?! No Unit Tests?! A solution with 180 projects?! Very-similar-but-with-slight-variations duplicated monster classes?! And you think to yourself: surely those people heard of the SOLID principles? Surely they’ve seen a webinar or two on Unit testing and TDD? They might even know what IoC and DI is, and the difference between them. How is it possible, then, that after more than a decade since we got all those wonderful acronyms people still write and maintain horrible mess?
The sad truth is, writing the perfect code is hard. It takes practice, years of experience in doing EXACTLY that – writing good code, and getting better at it. But we are all guilty of writing bad code. Because we don’t know any better. Sure, some are better than others, some have read a book or two on good software practices, some attend conferences and have open source projects – but they are the minority. Most of the time you will be the minority among your peers. Your colleagues don’t have time for your unit-testing nonsense, they have bugs to fix and deadlines to miss. If you are in a position of power, you may force some practices on your team because you have seen the light. And you’ll be lucky if they don’t hate you for it.
What does this have to do with unit testing? Well, it depends on who you ask. People who claim to have tried unit testing and found it too hard and a waste of time will generally roll their eyes in your general direction during yet another presentation on The Benefits of Unit Testing. People who have never heard about it before (or otherwise completely unfamiliar) will be intrigued – unit testing? Great! We want something that tests that our distributed systems can communicate with each other under load!, they exclaim. You politely smile and say: Well, not so fast! Unit tests are not actually testing anything! If only we could go back in time and remove the word testing from this practice, maybe it would be less confusing¦*.
And having seen the presentation, or read a book, or even attending a 3 day course on unit testing, those developers go back to their code bases, trying to apply what they had learned, and suddenly realizing that it’s too difficult! If only in the real world we were all building calculators and logging frameworks, surely the tests could be much simpler! But alas, we write distributed WCF systems, with transactions, states, workflows and proprietary protocols. How on earth are we going to unit test that?!
We as developers often forget our own guidelines and best practices, just because doing something right would be too much work. The WCF service now needs to communicate with another external system? Not a problem, just inject that interface (of course an interface, what do you think we are, amateurs?!) in the constructor (it already takes 9, but surely we can fit one more in there)! Code code code, debug debug debug, fix fix fix, debug debug debug, run, done. What did we forget? Of course, the damn unit tests! Now they are all broken because the new interface needs to be mocked and its behavior stubbed. And hey, didn’t we have that unit testing guy around? Let him fix it!
Which brings me to the actual point (if there is any) of this rant. It’s not just about unit testing. It’s about everything else too. I just tend to defend unit testing more because I am one of those who had seen the light, and I know exactly when to apply (or not) what I’ve learned. I have been practicing unit testing and TDD for years, but only recently had an epiphany. See, I always thought the problem with the term unit tests was the word test – people simply misunderstand the purpose of this – it’s not really testing anything, it is supposed to expose all the problems with using your API. Unit testing novices almost always will attempt to write the tests for the code they just wrote and will beat it into submission, until it passes. By that time, they have spent 20 minutes sweating, swearing and writing a very long test method, which has no real benefit or value.
I came to realize the real problem with the concept of unit testing. This realization came to me shortly after seeking help (see? I am constantly learning!) on refactoring a mess of a WCF service. Steven (@dot_NET_Junkie) posted an amazing, incredibly-detailed answer on how to approach this type of refactoring. By actually breaking responsibilities into smaller, self-contained components, or units, it suddenly became clear to me that the problem might actually be with the word unit itself – it is not a unit of work or a method or even a class, as it is often defined, but rather a level of abstraction. When all you have is a giant Manager class that takes 9 dependencies, and just one public method, the only possible way you can write a unit test for it is by directly invoking those dependencies – in essence, writing tests for the implementation details of the Manager class, and that’s a big no-no in unit testing – you should not write tests for the implementation details. This is why by adding a 10th dependency to the Manager class, you suddenly have to modify all your other tests to wire this dependency in, and mock it until it fits. This is why tests break and they become hard to write and maintain – they are not testing at the right level of abstraction, not at a unit level.
In this order: Better abstractions lead to better design. Better design leads to better unit tests. Better unit tests lead to quicker responsiveness to change. Quicker responsiveness to change leads to predictable release cycles of working software. And isn’t it what it’s all about?