Testable Design

Author: Gao Date: 2021-01-24
Testable Design

Guidelines for Testable Design

Testability issues

A programmer struggling to write a test is facing off against one of a few common show-stoppers. They mostly fall under two categories: restricted access, and inability to substitute certain parts of the implementation.

Can’t instantiate a class

This mostly happens with third-party libraries that weren’t designed with testability in mind. Another common cause for the inability to instantiate a class is that constructor depends on some static initialization.

Can’t substitute a collaborator

If the method is supposed to engage in an interaction with collaborating objects, but you find yourself incapable of intercepting the interaction you’re interested in. This might be because the collaborator is hard-wired into the method/class you’re testing and can’t be substituted with a test double.

Avoid complex private methods

You should strive to write private method so that you don’t feel the need to test your private methods directly, make your public methods read well, it should be perfectly fine that they get tested
only through those public methods.

Avoid final methods

The main purpose of marking a method as final is to ensure that it isn’t overwritten by a subclass, but often burden of worse testability outweigh the benefit of having the final.

Avoid static methods

A static method is easy to write, but you’ll have a hard time later if you ever need to stub it out in a test. Instead, create an object that provides that functionality through an instance method.

Use new with care

Every time you “new up” an object you’re nailing down its exact implementation. For that reason, your methods should only instantiate objects you won’t want to substitute with test doubles, it should be passed into the method somehow rather than instantiated from within that method.

Avoid logic in constructors

Constructors are hard to bypass because a subclass’s constructor will always trigger at least one of the superclass constructors. Hence, you should avoid any test-critical code or logic in your constructors.

Avoid the Singleton

There are situations where you want just one instance of a class to exist at runtime. But the Singleton pattern tends to prevent tests from creating different variants. If you do find yourself with a static Singleton accessor, consider making the singleton getInstance() method return an interface instead of the concrete class. An interface is much easier to fake. The much testable design would be that doesn’t enforce a single instance but rather relies on the programmers’ agreement or modern dependency injection framework.

Favor composition over inheritance

Inheritance does allow you to reuse code but it also brings a rigid class hierarchy that inhibits testability. If your intent is to reuse functionality it’s often better to do that by means of composition: using another object rather than inheriting from its class.

Wrap external libraries

Wrap the untestable piece of code of the external library behind your own interface, a thin layer that is testable that’s test-friendly and makes it easy to substitute the actual implementation.

Reference