How to isolate the system under test in C#?

Monday, March 8, 2021

In the last post, we looked at how to minimize untestable code from the architectural point of view. This is great for the long-term stability of your application. But imagine you have a simple class you want to test. Besides creating a new test class with your preferred Testing Framework, what do you do? How do you verify that you only test what you want to test? Today I want to take a quick look at isolation in code testing and answer the following questions:

  • What do we mean by isolation in unit testing
  • What are the benefits of isolation in unit tests
  • How does to isolate your class for testing

What do we mean by Isolation in unit testing?

Whenever we write unit tests, we want to make sure that we only test what we want to test. For this, we need to isolate the System Under Test (SUT) (reference) from its environment. The SUT is the function we want to test. In his book Xunit Test Patterns, Gerard Meszaros defines it as follows:

It is short for "whatever thing we aretesting" and is always defined from the perspective of the test. When weare writing unit tests the system under test (SUT) is whatever class (a.k.a.CUT), object (a.k.a. OUT) or method(s) (a.k.a. MUT) we are testing;

So if the SUT is "whatever thing we are testing" we want to avoid testing anything else. How could this isolation look like in a simple unit test? One option is to use dependency injection and inject Test Doubles into the class which we are testing. Test Doubles can be anything from Mock Objects to Test Stubs. I will go deeper into Test Doubles in another post.

Usually, you create the SUT during the setup phase of your test. Instead of injection real dependencies into your SUT, you inject Test Doubles which only provides minimal functionality. This means we can build up Test Doubles custom-tailored for specific test cases. Therefore we only focus our tests on what's important.

What are the benefits of isolation in unit tests?

In contrast to integration testing or end-to-end testing unit testing only focuses on a single unit for testing. For example, you want to know if a specific method you wrote behaves correctly without concerning about its dependencies. In this case, you control the dependency's behavior. This brings the following advantages:

  • Your tests are not sensitive to their context. Thus you don't have to worry about Fragile Tests.
  • Your Tests are independent of each other. So your tests are more robust.
  • You minimize untested code inside your application.

How to isolate your class for testing?

So in theory we now know what isolation is, how we could achieve it, and what the benefits are? But how can we implement a class in a way that benefits isolation? As I mentioned earlier, one approach is dependency injection. Especially constructor injection provides good flexibility when building up your SUT. For this example, I'm using C# to show how to construct a class and its corresponding test class.

GIST: https://gist.github.com/adrinamin/948c16b0c511e020ab49cfeac4e34d76.js

I'm using the CrossSumCalculator example from the previous post as it displays the isolation of the SUT very well. The  CrossSumCalculator class is injecting the IMessageHandler interface. The implementation of the MessageHandler is abstracted and we don't care about its implementation. The Calculate method simply gets a string and returns the cross sum as a double value.

If we want to write a unit test, we are trying to create a Test Double from the IMessageHandler. The easiest way to do this is by using the Moq framework and creating a new mock object. We don't care about displaying any kind of message to the user. We want to verify the result of the cross-sum calculation. Therefore a simple mock object would be sufficient in this case. Finally, our CrossSumCalculatorTests class would look like this:

GIST: https://gist.github.com/adrinamin/117ee81bfba7c7b71b9624d1a953227e.js

At the beginning of the test method, we create a new CrossSumCalculator instance. Because we want to isolate this instance we submit a Mock of IMessageHandler. Now the CrossSumCalculator is isolated from its environment and we are not dependent on any implementation changes of the MessageHandler component.

Conclusion

Isolation is one of many principles when writing good automated tests. Especially when writing unit tests you want to focus only on a specific unit and not thinking about the unit's context. So here are my key takeaways for the isolation of your  SUT:

  • Isolation makes your tests more robust and you are immune to context changes.
  • You try to replace the actual dependencies with Test Doubles which only provide the minimal functionality you need for your test.
  • The easiest way to replace your dependencies with your Test Doubles is through dependency injection

Let us know what you
think about this article