Don't Restrict Yourself to Mocks: Test your Components Running Together
Hi Friends.
So far, we’ve been looking at writing unit tests. These are great at checking that the individual parts of our app are working correctly. But sometimes we want to zoom out a little; we want to be sure that multiple components work when connected. We want something bigger than a unit test, but not encompassing the whole app – maybe because it hasn’t been completed yet, maybe because we want to stay focussed within a specific area.
Well, it turns out there’s a test for that.
In an integration test, we chain multiple components together and check that everything goes smoothly. If we cast our minds back, we were writing a small app to calculate the hypotenuse of a triangle. There were three steps in the calculation, and we’ve already covered how we might go about testing the first two:
Squaring a number. We did this with two different values.
Finding the sum of the two squares.
One step remains – finding the square root of the total.
The Final Part of the Calculation
As we’ve already created a CalculationService
, we can add a new method to it that joins everything together. Let’s start by adding a new test:
[Test]
public void CalculationServiceCanCalculateHypotenuse()
{
// Arrange
var multiplicationService = new MultiplicationService();
var service = new CalculationService(multiplicationService);
// Act
var result = service.Hypotenuse(3, 4);
// Assert
Assert.That(result, Is.EqualTo(5));
}
We’ve used a real instance of MultiplicationService
in this test. This contrasts with our previous tests where we created testing mocks using the Moq library. With the test out of the way, we can now add a new method to CalculationService
to implement the missing functionality.
public double Hypotenuse(int a, int b)
{
var sumOfSquares = SumOfSquares(a, b);
var hypotenuse = Math.Sqrt(sumOfSquares);
return hypotenuse;
}
Note that we’ve used an already-existing C# library method (Math.Sqrt
) to find the square root. As it’s not something that we’ve written ourselves, there’s no way to mock out that method – nor should we. We can assume that the C# library maintainers have already tested it. And even if it did contain bugs, there’d be no direct way for us to fix it.
As shown in image 1, we should see that our test passes when we run it.
Integration vs Unit Tests
In a unit test, we typically isolate the system under test (SUT) by substituting all its dependencies. The previous articles in this series did this by mocking; there are other types of test substitute, but we won’t go into those now. Isolating the SUT helps to narrow our search if things go wrong, as there are no other components/dependencies involved.
In an integration test, we use actual instances of components to check that a process has the expected outcome. By testing the output of more than one component, we can check whether those components produce the desired outcome when working together. However, if the test fails, we only know that something in the chain is misbehaving. We have less accuracy locating the point of failure at a glance.
As such, one type of test isn’t ‘better’ than the other – they complement each other. Unit tests ensure that individual components function correctly, while integration tests check that chains of components generate the right outcome. As integration tests involve more than one component, they might take longer to run.
It’s important to note that an integration test doesn’t have to include all the components of an entire workflow. It’s also possible (and often a good idea) to test smaller chains of components too. By doing so, individual tests can take less time to run. Having smaller component chains also helps to localise areas to investigate should a test fail. In tests where we don’t test an entire workflow, we can use substitutes such as mocks to replace dependencies at either end of the component chain where necessary.
Summary
Unit tests have a narrow focus. Integration tests allow you to zoom out. This lets you test parts of your app running together as they would when deployed. While running multiple components allows you to test a workflow (or a part of it), there may be some disadvantages. Depending on the components involved, tests could take longer to run. Also, it’s potentially less clear where the problem lies if a test fails, as there are multiple components involved.
As such, it’s a good idea to have both unit and integration tests in your project. By taking a multi-layered approach, you can benefit from the best of both worlds.