Refactoring is the act of changing the code without changing the code’s functionality.
Before we get started, let’s go through the definition of the objects used in a automated testing:
- Mocks are simplified objects of their production counterparts which look and behave like the real object. It can be asserted against a mock object.
- Stubs are replacement objects for a dependency for testing the code without using the dependency directly. It cannot be asserted against a stub.
- Fake is an object that looks like another object and can be used as a mock or stub. Fakes can be written by hand or generated by a framework.
- Seams are places in the code where different functionality can be injected, like stubs, by adding a constructor parameter, a public settable property, making a method virtual so it can be overridden, externalizing a property so it can be set from outside. (Open-Closed Principle: open for extension, closed for direct modification.)
One of the most common problems why code might not be testable are dependencies. Dependency is when the object under test relies on or interacts with another object that is not under the control of the test. The test can’t control what that other object returns to the code under test and how it behaves. Test-inhibiting design is when the code has a dependency which might break the test even though the code is correct. In order to solve this problem, stubs are used.
The “art” on the art of unit testing is finding the right place or use a layer of indirection to test the code base.
If something cannot be tested, either a layer is added to wrap up the call to that something and mimic the layer in the test or that something is made replaceable. The replacement will not talk directly to the dependency so the dependency is broken.
How to introduce testability in your code
Avoid using base classes instead of an interface because this base class may already have built-in dependencies that have to be overridden.
There are two types of dependency-breaking refactorings:
- Type A – Abstract objects into interfaces or delegates
- Extract an interface to allow replacing the implementation
- Type B – Refactor to allow injection of fake implementation of those interfaces or delegates
- Inject stub into class under test
- Inject fake in the constructor
- Inject fake as a property get or set
- Inject fake before a method call
Dependency injection is commonly used to inject a fake implementation into the unit under test. The places to inject an implementation of an interface into a class to be used in its methods are:
Receive the interface in the constructor and save it in a field
This is a design choice because these parameters are non-optional so instances have to be sent in the constructor for all dependencies that are parameters in this constructor.
The code can become less readable and less maintainable if more parameters are added to the constructor for each dependency or if more constructors are created for each dependency.
Alternatively an object can be passed into the constructor that contains all relevant dependencies as properties (parameter object refactoring) but also this approach can get out of hand when the object has many properties.
Another option would be using Inversion of Control or IoC containers that initializes the object in special factory methods based on the type of object to be created and its dependencies. These containers use special configuration rules like which constructor to call, in which order to set properties and so on.
Receive the interface in the property get or set and save it in a field
If the dependencies are option then using property getters and setters is a more relaxed way to define dependencies. Each dependency that is injected has a property which is then user when needed in the code under test. The dependency typically has a default instance created that won’t cause any problems during the test.
Receive the interface before the call in the method under test
This can be accomplished by using the Factory pattern where another class is responsible for creating the object. The factory class will be configured to return a stub in the case of the test.
However factory classes typically don’t allow changing the instance being returned outside of the factory in order to protect the encapsulated design of the factory class. this can be solved by adding a new setter method or seam in the factory class so that the test has more control over which instance this factory class returns. In the case of the test it would be the stub instead of the default implementation.
In this case each class is responsible for one action so separation of concern for the classes is achieved.
Local factory method
By using a local virtual method in the class under test, this can be overridden in a derived class on which the tests are then performed. The derived class is the stub object that overrides the virtual factory method to return the instance required in the test, making this a stub method. When testing the class, the production code will be using the fake via the overridden factory method.
This technique, also called Extract and Override, allows directly replacing the dependency without many changes to get the code into a testable state. It is suitable for simulating inputs, return values or whole interfaces into the code under test. It is however not good for checking interactions between objects.
Parameter in the method (parameter injection)
By adding a parameter for the dependency to the method, the test can send an instance of the fake dependency to the method.
Layers of indirection
The deeper it goes down the call stack, the better manipulation power it has over the code under test. However the farther it goes down the layers, the harder in will be to understand the test, the code under test and the interaction between objects and the harder it will be to find the right place for the seam. Finding the right balance between complexity and manipulation power is the key for readable tests and full control of the code under test.
- Layer 1: faking a member in the class under test
- Layer 2: faking a member in a factory method
- Layer 3: faking the factory class
Testable object oriented design
Adding constructors, parameters, properties, methods and factories for testing purposes breaks the object-oriented principles, especially encapsulation. These principles ensure that the end user of the object API is using the object model properly and that this model is protected from unintended usage.
“Hide everything that the user of the class doesn’t need to see.”
However by writing tests, a new end user is added to the object model and this user has different goals and requirements when using the model that defy some of the object-oriented principles, like encapsulation. Designing with testability in mind, called TOOD or testable object-oriented design, has the advantage of being able to write tests against the code.
While TOOD conflicts with the concepts of OOD, there are ways to make sure that the additional constructors, properties and methods don’t end up in release mode, like using internal and [InternalsVisibleTo], the [Conditional] attribute and #if-#endif for conditional compilation.
You can find the source code to the examples from the book as a zip file in the Free Downloads section directly at the webpage of the book at Manning or on GitHub at Art of unit testing first edition code samples for vs 2010
Resource for this article
- The Art of Unit Testing, Roy Osherove
- Working effectively with legacy code, Michael Feathers
- Clean code, Robert C. Martin
- Dependency injection in .Net, Mark Seeman
- Design patterns, Gang of Four
- Factory method and Abstract Factory Design patterns