Sytuacja kobiet w IT w 2024 roku
15.10.20207 min
Wojciech Tobiś
Relativity

Wojciech TobiśSoftware EngineerRelativity

Testing the untestable

Find out how to approach testing of elements that are difficult or almost impossible to test.

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
<p>Loading...</p>