Unit testing is something I have been doing for many years now. During that time, I have learned a lot about writing good tests, clean code and testability in general. I have learned mocking, TDD, BDD, you name it. One thing however has stayed the same during the whole learning process: the concept of unit. When I started unit testing I learned that class is a unit and all the external dependencies of the class should be mocked away. So, each class is tested in isolation from all the other ones and there is always a test class for each production code class.
This definition of the unit is quite popular and a good guideline for developers learning the secrets of automated testing, mostly because it leaves no room for interpretation. While it has served me well on my journey becoming a better developer, during the past year I have started to feel that it might not be the best unit testing strategy for me after all.
Let me go through the reasons that have lead me to the search for more suitable unit than a class.
For me, one of the main benefits of the unit tests is that they enable safe refactoring of the code. The conflict I have faced with the class level unit tests and refactoring is the following. Most of the refactoring I tend to do, is reorganizing code towards some design pattern I notice applicable for the situation, or when I find new responsibilities and extract those into their own classes. None of this refactoring is happening in a scope of a single class. Therefore, I have to modify the unit tests every time I change the design, which is a sign of fragile tests. Tests that probably test implementation rather than behavior. The tests become a burden that must be kept in line with production code every time refactoring is happening. New tests must be created for new classes, some removed and the others updated. While doing all these changes to the test suite at the same time I refactor the production code, I find it difficult to feel safe doing the refactoring.
Learning test driven development has been a bit of struggle to me, to be honest. The idea in a simplified form is that you write a test, you write production code and when the test is green you can go and refactor. When doing TDD, the refactoring step almost always leads to extracting a class from the class you started with. At that point, what should you do? Create a new test class for the extracted class and start to do TDD with it? If class is your unit of unit testing, then this is the way to go. With this strategy I have found that I end up writing a lot of unit tests that just assert that the extracted classes are called with the correct parameters and the return values of those extracted classes are processed correctly. These are tests that don’t test the domain behavior as much as they test code behavior. These are the tests I need to modify each time I want to refactor the code, just because of the fact that they depend on design of the application, not the domain behavior.
The second conflict I have experienced with TDD and class as a unit, is that it ruins my TDD experience. And I care about my experience! I find it hard to focus on implementing the behavior I started with, when I need to write these did-it-call-that-with-those-parameters tests in between. That said, I still test unit in isolation and there are boundaries where mocks must be applied. In those boundaries I still test that the calls are made with expected parameters and that the inputs from the mocks are properly used.
Size of the unit
Being a fan of Clean Code has affected a lot of how I write code and how my code looks like. Over time I’ve learned to write classes that better follow the SOLID principles. That has made my classes a lot smaller than they used to be. Many classes you can fit on the screen or two and the methods in those are even smaller. Usually no more than 5 to 10 lines of code. In other words, the size of the unit has dramatically shrunken as I have learned to organize my code better. This is a good thing, no doubt. The problem is that not always one class alone implement a meaningful domain behavior. Indeed, separation of concerns usually leads into many many small pieces that together do something meaningful. I find it more meaningful to test those bigger behaviors (still not that big!) than the pieces and their boundaries.
It’s all about trade offs. My biggest concern was that I would loose the exact pin-point power of the unit tests. You know, they tell exactly where the issue is. That, however, never materialized in practice. If you run your unit tests continuously as you develop, then you can be pretty sure that the few lines of code you just added before test execution is the reason of failing test. This applies weather the scope of the unit is one class or few classes. Also the execution time of the test suite stayed the same.
All that said, of course there are still classes that contain significant amount of logic that deserves to be tested in isolation of all the other code. The change I have made is that I’m not that dogmatic about it anymore. I try to find better, more meaningful units and tests those in isolation. If you are in doubt, I say, try it! Time will tell if this works out great or will I revert to dogmatic class level unit testing. So far, it has worked for me.