Top 15 Patterns and Practices of Unit Testing

Top 15 Patterns and Practices of Unit Testing

How can you elevate your unit testing practice? What follows are 15 of the top patterns and practices of unit testing I have developed over the years. Some of them will be new to you, a few of them may be controversial. Please take what works for you and discard the rest.

Use Single Responsibility Principle

Use Single Responsibility Principle in your test classes just as you do in your production code. Test projects, test files, test classes, test methods, test assertions - each of these must have one and only one responsibility. This does not mean a test should have a single assert as some have proposed. It means that the test should test one behavior.

Use Sentences for Test Names

Use sentences for test names. They should specify member, behavior, preconditions, and post-conditions. For example, AuthorizeTokenMustThrowExceptionWhenTokenIsNullOrWhitespace. Whether you use BDD-style sentences or some other pattern is less important than capturing all four categories in the name.

Be Consistent Within a Context

Consistency should be viewed from the perspective of current and future team members. Inconsistency in naming, layout, etc. results in a less understood bounded context.

Avoid Use of Static Methods

Avoid use of static methods. They cannot be overridden. They are cumbersome to mock. They may leak side effects when used in a suite of tests.

Prefer Virtual Methods

Prefer virtual methods as they make for more testable code, especially if the code is in a shared package. You as the developer of reusable code should not restrict other developers from the ability to mock or override your methods as part of their own testing.

Avoid Use of Mocking Frameworks

Avoid use of mocking frameworks where possible. It is trivial to write your own test doubles (which have no overhead). Mocking frameworks often incur hundreds of milliseconds of overhead. Multiply that by 1000, 5000, 10000 tests and the additional overhead becomes significant. This has two serious impacts. Your build times will get longer and developers will stop running all the tests as frequently as they should.

Reduce Runtime Overhead

Identify and minimize runtime overhead. For example, in-memory databases must generate the model for every context the first time a context is used. Similar to mocking frameworks, this incurs hundreds of milliseconds of overhead and thus does not scale.

Share Useful Testing Patterns

Be sure to refactor helpful testing patterns to common test libraries. This will enable good patterns to proliferate and reduce the potential for poor patterns.

Go Ahead and Repeat Yourself

Common testing patterns are great. Not-so-common testing patterns are horrible. Prefer private patterns until you have derived a truly common test pattern. There is no harm in having multiple MockProducer classes if their behavior is particular. There IS HARM in forcing a single MockProducer to juggle multiple testing responsibilities. This makes for brittle test code.

Minimize Console Output During Test Execution

Do not allow logging and console actions to be sent to standard out during test execution. Without this practice, important build messages may be ignored. Carefully mock standard out and console logging providers.

Verify Logging as Part of Tested Behavior

Generously assert logging output. If unit tests protect from unwanted released behavior, then logging aids in improving already released behavior. Your tests must assert what you expect from your logging. Think of it from the perspective of: what do I want my system to log when I am troubleshooting production at 3 am? Write tests for that!

Establish an Average Test Duration

Unit tests should take 100 milliseconds or less to complete. If tests are taking longer than that, the method under test may need to be optimized and/or the testing strategy may need to be altered. You will have thousands of tests and you want them all to complete in a couple minutes or less.

Now Is Your Chance to Pass Null

Null is your friend when writing unit tests. Do not be afraid to pass null to a method under test. If the parameter is nullable then there is no need to spend time creating a value for it - instead focus on the parameters which are relevant to your test. Another benefit of this null-passing is that you implicitly assert the nullability of the parameter. If that changes in the future, your test will alert you by failing.

Avoid Use of @Ignore

Applying the Ignore attribute to a test is tantamount to commenting it out. If the test is truly no longer useful, remove it. Never ignore failing tests, especially intermittently failing tests. Remember that a failing test is a good thing - consider it a great opportunity to fix an issue before it progresses through the delivery pipeline.

Run All Tests Before Pushing

Run all tests before pushing. Ideally you should use a script that requires all tests to pass before allowing commits to be pushed.

Conclusion

I have been writing software for well over 30 years. I have been writing test-oriented software for over 20 years. Much of my practices and patterns were originally informed by Kent Beck and Uncle Bob, and later by Johannes Link. I mention this only because over the years I have discarded some things and added others. Where my patterns and practices deviate from the better-known authors, consider that those differences are born out of my hard-earned experience in the trenches.

I hope some or all of the above helps elevate your own testing practice - as well as your software engineering in general.