Test should drive, not block functionality changes
When tests are too coupled to the implementation code, changing functionality becomes not only harder but also riskier as a lot of changes means the risk of missing or invalidly correcting tests. Before you know it, you end up with a test suite that is either ignored during its run or ignored during development of new features.
Tests should drive functionality, it should give confidence and be a motivator for functionality changes. One way to do this is to remove unnecessary relationships with the implementation code directly. What went wrong with TDD over the years is that people think an ‘unit’ is a ‘class’ or ‘method/function’, while it is actually an ‘unit of behavior’. Coupling implementation in the test for every method of every class, just for the sake of having tests, only blocks changed functionality as the effort in changing duplicated, unnecessary and tightly-coupled test code is very big.
Reconsider using Moq
One of the tips I give other testers is to not be fixated on the Moq library. If all your tests look like setting up stubbed-out dependencies and then verifying if those dependencies are called in the right way, you have very tight-coupled tests that in fact does not test the behavior of your implementation, only the behavior of the Moq library. It is about changing mindset gears and look at your implementation from an outsider. ‘What do I expect?’, ‘What should it do?’, ‘How do I know that it works?’
If you stop thinking about Moq for a second and pay attention to the required state, the necessary input data, the expected results; you will often see that tests outgrow these simple stubbed-out dependencies from Moq in favor of testing the behavior instead.
Reconsider direct relationships
The direct relationships in OOP code is often the constructor of a class and the method under test. Imagine if one of those need to change, what is the impact on the test suite? How many tests need changing? If this is more than a couple, then you might reconsider these direct relationships.
Often, not all constructor dependencies are directly important for the test scenario. Loggers, for example. Exposing these dependencies to the direct test body is not a good idea for several reasons: it unnecessary complicates the test body with noise, but also adds an additional relationship to the implementation code. If you extract the constructor to a factory method and only pass in the necessary dependencies interesting for the test, you removed the noise and minimized the relationship. Now, the constructor can change independently from the test.
The same often goes for the method under test. Maybe not all passed arguments are interesting for the given test scenario. Creating an extension on your class might help here, to show that you are actually still running functionality on the type, instead of hiding it just in a method.
Reconsider testing overloads
Especially in library development, you often have multiple overloads that exposes the same functionality just in a different format and/or with additional options. But in essence, it is the same functionality. To show this clearly in the test, it is not a good idea to duplicate tests from one tested overload to another. I am even favor of completely hiding from the test body which overload the test is using. Only when the options change functionality, should this be shown in the test body, in my opinion.
To hide these overloads from your test body, but still test all of them, I often use a combination of property-based testing and randomization. A single test is run multiple times in a single test suite run with randomized inputs and in the same way each time an overload is picked randomly. This clearly shows that ‘it does not matter’ which overload, and also removes the need for the test name to be specific which overload is being used.
Conclusion
These simple changes to your test suite help minimizing the relationship with your implementation and also makes you think more about expected behavior instead of just duplicating implementation code. The less relationship there is with your implementation, the more the implementation and tests can change independently from each other.
Thanks for reading!
Stijn
Subscribe to our RSS feed