Testing the untestable
Several weeks ago, I found a great article written by my colleague Konrad Gałczyński. He's described some problems with extension methods. They have a significant impact on unit tests, both their own and for classes that use them. Based on this impact and possibility (or impossibility) of creating unit tests, Konrad defined three buckets of extension methods and guided how to be "Good citizen". There are also some interesting real-life examples of untestable functions that really help to understand the problem.
After reading it carefully, I realized that these problems are more general, not related just to extension methods. Codebase in my current project had some branches, that were not accessible in standard unit tests, and there was no easy way how to solve this issue. It would be really profitable if all these places were detected and resolved, so I was thinking about what root cause would be.
Let's talk about mocks, baby!
In the mentioned article, extension methods are divided into three groups due to the ease of testing; both methods themselves, and their usage.
- Good citizen– easy to test
- Neutral fellows– more difficult, but still possible
- Mighty villain– almost impossible to test
I’ve tried to put each of my problematic cases in one of these buckets and then fix them. But this task was quite hard and not natural for non-extensions, so we need a new classification.
Before that, let’s define what a unit test is and what it should do. Tests on a unit level should:
- check how public methods of a component under the test work
- not depend on any third-party components – any dependency passed by property or in a constructor should be mocked
- not test private methods explicitly – they are just helper methods for public ones
The last point is extremely important in order to validate extension methods because from the testing point of view, they are nothing more than private methods moved into a separate place. It implies that almost everything that applies to extensions also applies to private methods. Moreover, taking into consideration the second point, we realized that the most difficult thing of testing extension methods (and private ones as well) is the control over the component’s dependencies. Having that and examples provided by Konrad, we can redefine the buckets:
There's nothing wrong with Neutral fellows and we could not avoid them. The real problem is with Mighty villain, but we can easily solve it.
Reasons of villainy
Before we start finding a solution, we must ask ourselves: what makes dependencies unmockable? These are some of the main factors:
- method that creates an object with its constructor explicitly
Explicit call of constructor
public void MethodWithNew()
{
var nestedGreetingsService = new NestedGreetingsService();
nestedGreetingsService.SendGreetings("Hello world!");
}
- method that uses static method
Method with static method
public void MethodWithStaticMethod()
{
StaticGreetingsService.SendGreetings("Hello world!");
}
- method that uses an object which is a constructor argument and this argument is a class, not interface.
Class with another class as constructor argument
public class WelcomeService
{
private readonly GreetingsService _greetingsService;
public WelcomeService(GreetingsService greetingsService)
{
_greetingsService = greetingsService;
}
public void MethodWithInstance()
{
_greetingsService.SendGreetings("Hello world!");
}
}
- method that uses extension methods from a third-party library
Method with third-party extension method
public void MethodWithThirdPartyExtension()
{
string greetings = "Hello world!";
greetings.ThirdPartyExtensionMethod();
}
We have to be aware that these are just basic reasons and there are probably more complex ones. For each of these factors, we could not provide any mock. Its breaks do not depend on any third-party components’ rule for unit tests and as a result, these tests could have some side effects (time-consuming operations, HTTP calls, SQL operations, etc.). We should instead check if dependencies are used in the right way then observe their results, what is typical for integration (and above) tests. Unfortunately, it’s impossible without mocks.
Test for method with static method
[Test]
public void MethodWithStaticMethod_ShouldSendGreetings()
{
// Arrange
var sut = new WelcomeService();
// Act
sut.MethodWithStaticMethod();
// Assert
}
Bearing in mind these facts, let’s analyze the possible solutions.
What not to do
Reflection
After a few unsuccessful attempts, I’ve tried to use reflection. It can do anything, can’t it? I’ve even found SwapMethodBody to update the tested method. But I realized that it’s a dead end. This would be no more than an extremely inelegant, force solution. What’s more, it could make tests slower and more difficult to read or maintain just because of the explicit constructor call or static method! That’s a symptom of potential architectural issues and using reflection would be overkill.
Using execution wrappers
The second attempt was the usage of an execution wrapper. It’s a function that accepts another function as an argument and invokes it (usually with some additional actions).
Simple execution wrapper implementation
public void Execute(Action action)
{
action.Invoke();
}
A good, real-life example of them is transaction wrappers (for SQL transactions in C#) and retry policies. In this case, we can provide a fake implementation of an execution wrapper and then verify if our dependency was utilized correctly.
Test for method with execution wrapper
[Test]
public void MethodWithInstance_ShouldSendGreetings()
{
// Arrange
var greetingsService = new GreetingsService();
var executionWrapperMock = new Mock<IExecutionWrapper>();
executionWrapperMock
.Setup(m => m.Execute(It.IsAny<Action>()));
var sut = new WelcomeService(greetingsService, executionWrapperMock.Object);
// Act
sut.MethodWithInstance();
// Assert
executionWrapperMock
.Verify(m => m.Execute(It.IsAny<Action>()), Times.Once);
}
Well, it finally works. However, this solution is still imperfect because:
- we should not use existing wrappers (e.g. transaction wrapper or retry policy) just for checking other dependencies usage – they are not responsible for it
- we could use our own implementation, but it’s better to verify dependencies themselves (not with an additional thing)
- it’s easy to check dependency execution, but much more difficult to examine its arguments
- it’s just a workaround to make tests work, but doesn’t improve them, or even worsens anything from readability or an architectural point of view
This solution would be acceptable, but we can do it better.
What to do
Before we move forward, it’s necessary to discuss one crucial observation. Every component with nonmockable dependencies is transformable to a component with mockable dependencies, that have nonmockable dependencies.
What is more, these nested dependencies could be standardized. On the other hand, we must accept that there will be some Mighty villains, but in a well known and expected place and form. Sounds really complicated, but it’s very easy to use.
Factories
First of the acceptable Mighty villain is a factory. It’s a great way to create objects because we can mock it to return another mock.
Factory example
public class NestedGreetingsServiceFactory : INestedGreetingsServiceFactory
{
public INestedGreetingsService GetNestedGreetingsService()
{
return new NestedGreetingsService();
}
}
Factory usage
public class WelcomeService
{
private readonly INestedGreetingsServiceFactory _nestedGreetingsServiceFactory;
public WelcomeService(INestedGreetingsServiceFactory nestedGreetingsServiceFactory)
{
_nestedGreetingsServiceFactory = nestedGreetingsServiceFactory;
}
public void MethodWithFactory()
{
INestedGreetingsService nestedGreetingsService = _nestedGreetingsServiceFactory.GetNestedGreetingsService();
nestedGreetingsService.SendGreetings("Hello world!");
}
}
Then we can verify e.g. invocation’s count or arguments.
Test for method with factory
[Test]
public void MethodWithFactory_ShouldSendGreetings()
{
// Arrange
var nestedGreetingsServiceMock = new Mock<INestedGreetingsService>();
nestedGreetingsServiceMock
.Setup(m => m.SendGreetings(It.IsAny<string>()));
var nestedGreetingsServiceFactoryMock = new Mock<INestedGreetingsServiceFactory>();
nestedGreetingsServiceFactoryMock
.Setup(m => m.GetNestedGreetingsService())
.Returns(nestedGreetingsServiceMock.Object);
var sut = new WelcomeService(nestedGreetingsServiceFactoryMock.Object);
// Act
sut.MethodWithFactory();
// Assert
nestedGreetingsServiceMock
.Verify(m => m.SendGreetings(It.IsAny<string>()), Times.Once);
}
Testing factories is also possible, but just the object creation’s logic rather than the created object’s logic.
Wrappers
The best option for a static or third-party extension method and the rest of the mentioned cases is a dedicated wrapper. Unlike the execution wrapper, its responsibility is just to wrap method call, without any side effects. Otherwise, these effects could become untestable.
Wrapper example
public class StaticGreetingsServiceWrapper : IStaticGreetingsServiceWrapper
{
public void SendGreetings(string greetings)
{
StaticGreetingsService.SendGreetings(greetings);
}
}
Wrapper usage
public class WelcomeService
{
private readonly IStaticGreetingsServiceWrapper _staticGreetingsServiceWrapper;
public WelcomeService(IStaticGreetingsServiceWrapper staticGreetingsServiceWrapper)
{
_staticGreetingsServiceWrapper = staticGreetingsServiceWrapper;
}
public void MethodWithWrapper()
{
_staticGreetingsServiceWrapper.SendGreetings("Hello world!");
}
}
Wrappers are extremely important because, same as factories, they could be mocked. Thanks to this fact, we can verify invocations, returned objects, etc. without unexpected operations.
Test for method with wrapper
[Test]
public void MethodWithStaticMethod_ShouldSendGreetings()
{
// Arrange
var staticGreetingsServiceWrapperMock = new Mock<IStaticGreetingsServiceWrapper>();
staticGreetingsServiceWrapperMock
.Setup(m => m.SendGreetings(It.IsAny<string>()));
var sut = new WelcomeService(staticGreetingsServiceWrapperMock.Object);
// Act
sut.MethodWithWrapper();
// Assert
staticGreetingsServiceWrapperMock
.Verify(m => m.SendGreetings(It.IsAny<string>()), Times.Once);
}
Wrap up
We've discovered some standard ways to write unit tests for a class that seems to be untestable. This approach helps us to increase code coverage and improve application reliability. The most important things to remember:
- All described rules apply to extension methods as well as other methods in a class
- There are some well-known factors that always break tests
- Reasons of villainy described above are not the only ones - feel free to contact me and provide more examples
- Using reflection is overkill in this case
- An execution wrapper is an option but should be treated as a workaround
- Not every class is mockable, but we can keep unmockable ones in an expected form
- Every component with unmockable dependencies is transformable to a component with mockable dependencies, that have unmockable dependencies