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:
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.
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:
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.
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:
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.
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:
A Testing API can help you decrease the structural coupling between your production code and your test code. In the long run, it will save you much time! A bold claim, of course, and it sounds very theoretical. To provide you with more depth than just a basic concept, I want to show you today how you can implement a Testing API with .NET.
Structural coupling makes refactoring our code very difficult and it costs us a lot of time and money! But there is one solution: a Testing API. It's an API with superpowers which helps you to get rid of this structural coupling. At the same time you are still able to verify your business rules. This can be a real time-saver!