Unit Testing - Anti Patterns
Bad unit testing practices can cause a test suite to be no longer fit for purpose by inducing frustration in developers and slowing down development, this among a range of other factors can ultimately lead to abandonment of unit testing altogether.
In this post I go through bad practices and misconceptions around unit testing; Spreading awareness can aid in avoiding falling victim to such practices.
“If there is a recurring bad solution to a common problem, then by documenting it we can prevent other developers from making the same mistake. After all, avoiding bad solutions can be just as valuable as finding good ones!” – Headfirst Design Patterns
Code quality: A common misconception is that code quality does not matter within a test suite and it is okay for it to be treated as a second-class citizen. This is a misconception because the messier the test code becomes the more difficult and time-consuming it becomes to change.
“Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code”. – Robert Martin
100% Code Coverage: Code coverage does not correlate to the quality of a test suite, as code coverage metrics do not cover logic branches on a single line (inline if-statements) and they also do not cover all possible outcomes. Eventually, such hard-core rules result in tests with no value as developers aim to satisfy the metric.
“Coverage metrics are a good negative indicator but a bad positive one. Low coverage numbers are a certain sign of trouble, but a high coverage number doesn’t automatically mean your test suite is of high quality.” – Vladimir Khorikov
Repeating sections: multiple arrange, act, or assert sections indicates that the test attempts to verify more than a single unit of behavior, such tests can no longer be considered unit tests, rather they are integration tests.
Conditional Logic in tests: Introducing conditional logic into tests such as if statements increase the maintenance cost and make a test more difficult to read while also indicating that a test is attempting to verify too much.
Shared dependencies: Introducing a shared dependency between tests violates the isolation properties of unit tests. The dependency is often introduced as an attempt to reuse setup code between tests, however, better solutions exist such as utilizing the Object Mother and Test Builder patterns.
Algorithm duplication: Duplicating a production algorithm in a test method results in a white box test that is coupled to implementation detail such tests are brittle, the focus should rather be on the outcome.
Strict Mocking: strict mocking requires tests to ensure that each interaction with a collaborator is expected, this results in extremely brittle tests where a minor change within will lead to many false positive failures.
Database Mocking: Testing against in-memory databases or mocking a database in general leads to tests that run in completely different circumstances to production environments as the database engine and constraints vary greatly, therefore aim to test against the same database engine your production code targets as part of integration testing efforts rather than unit tests.
Exposing private methods: Private members are not part of the observable behavior therefore they should not be exposed for the sake of testing. This unnecessary exposure leads to a violation of encapsulation principles and makes decreases the tests' resistance to refactoring. Testing the observable behavior will inherently test private members as they lie within the execution path generated as a result of invoking public members.
Unit testing speeds up development when done right, the opposite is true when one goes about unit testing the wrong way.
“The end result for projects with bad tests or no tests is the same: either stagnation or a lot of regressions with every new release.” – Vladimir Khorikov