If you have a kitchen, and that kitchen has drawers, there is a roughly 100% chance you have a junk drawer. You know the one, where you store old phone cables, ribbons, safety pins, expired coupons, and a broken red crayon. The software equivalents of these junk drawers take many forms, commonly named
Utils or the worst offender of all.. the
TestBase. Join me as we take our new found love of dependency injection, and actually put it to good use.
The dreaded TestBase
This is something that becomes more prevalent above the unit test layer and more common in an integration test layer. At a certain point environmental resources, external state, and other moving parts are going to need to be initialized for tests to validate specific scenarios. After a few methods repeating the same code, the
TestBaseOfUnusualSize is created.
It definitely makes sense to minimize code boilerplate when generating tests in the first place, but it comes at a cost of context to later readers. As systems grow and become more complex, different portions will only have partially overlapping resource requirements. Eventually the system ends up with a test base that is this thorny tentacled monster that just works, as long as you don’t look too hard at it.
Dependency injection has a purpose
This is where dependency injection refactoring will help. Ultimately, these large test bases can be replaced with a module, or several, that define the requirements for a test suite. Instead of spending every test suite run initializing a requirement used by only 5% of unit tests, the module can omit or mock unimportant subsystems. In the previous example we had to wait for a forest to grow for every test suite even though everyone knows a hedge maze in the arctic would be made with Boxwood!
The first step is to realize that the
FoodDeliveryService initialization could be done internal to a subclass that lives in the test package.
The end of magic
By extracting a reusable
FoodDeliveryService subclass, we still have the reusable code from the test base. We have also created a visible area of the test suite that explicitly defines all of the expectations of system state. No more magical setup, and hopefully a reduction in heisenbugs.
Another indicator that this form of refactoring may be applicable is the existence of the
@TestOnly annotation. If there is a class that has this feature, could a subclass solve the same requirement? Is the
@TestOnly method resetting a singleton’s internal state as opposed to just generating a fresh object graph? Don’t be afraid of test only subclasses, they function as documentation of expected states and allow for more composition friendly test suites.
@TestOnly methods make for quick, short PRs that can speed up other refactoring tasks. Remember, keep your refactoring changes on the small side for faster reviews and more stable releases.