DEV Community

Cover image for Isolate your Components in Tests: How to Mock your Dependencies
Anthony Fung
Anthony Fung

Posted on • Originally published at webdeveloperdiary.substack.com

Isolate your Components in Tests: How to Mock your Dependencies

We previously mentioned that unit tests act as basic checks for our app’s components. We also looked at how to write tests where we passed values into a calculation and compared the outputs with expected results. In these tests, we fed our sample inputs directly into a method that took a number, squared it, and then returned the result. No additional components were needed in the process.

However, things aren’t always that straightforward.

Sometimes, the logic in our services will rely on more than one component. In fact, it’s often a good idea to separate responsibilities when designing software modules. By keeping a module’s logic focussed to a specific area, it has a single identifiable purpose. This in turn means that we can reuse that module wherever it’s needed without having to bring in other features and capabilities that may not be immediately relevant. Furthermore, we can name the module more meaningfully, potentially making the codebase easier to understand.

In a unit test, we want to test a single component only – not a workflow. If the subject under test has dependencies, we deliberately want to exclude them from the test. That way, we’ll know exactly where to focus our debugging efforts if there’s ever a problem. One way to do this is Mocking.

The Problem so Far

We started the series by wanting to be able to calculate the hypotenuse of a triangle. To do so, we would need to do three things:

  1. Multiply a number by itself to find its square. We would do this for two numbers.

  2. Calculate the sum of the two values obtained from step 1.

  3. Find the square root of the result from step 2.

As part of completing step 1, we created a MultiplicationService that could square any given number. Focussing on step 2, let’s create a new CalculationService that:

  • Accepts two separate numbers as input values.

  • Squares them in the MultiplicationService.

  • Adds the two results together.

The code for this might look like the following:

public class CalculationService
{
    private readonly IMultiplicationService _multiplicationService;

    public CalculationService(IMultiplicationService multiplicationService)
    {
        _multiplicationService = multiplicationService;
    }

    public int SumOfSquares(int a, int b)
    {
        int aSquared = _multiplicationService.Square(a);
        int bSquared = _multiplicationService.Square(b);
        return aSquared + bSquared;
    }
}
Enter fullscreen mode Exit fullscreen mode

Writing the Test

We can see that SumOfSquares makes use of Square from MultiplicationService. To ensure that we only test logic within the SumOfSquares method, we can use a library called Moq to mock out MultiplicationService in our C# unit tests. To do this we first create an interface for MultiplicationService that contains all of its public methods:

public interface IMultiplicationService
{
    int Square(int number);
}
Enter fullscreen mode Exit fullscreen mode

Once MultiplicationService implements it, we can write our unit test. A first attempt might look like this:

[Test]
public void CalculationServiceCanCalculateSumOfSquares()
{
    // Arrange

    var multiplicationService = Mock.Of<IMultiplicationService>();
    var service = new CalculationService(multiplicationService);

    // Act

    var result = service.SumOfSquares(3, 4);

    // Assert

    Assert.That(result, Is.EqualTo(25));
}
Enter fullscreen mode Exit fullscreen mode

However, we’ll see that the test fails if we run it:

Expected: 25
But was:  0
Enter fullscreen mode Exit fullscreen mode

Why did this fail?

If we look at our test, we can see that we provided a mock of IMultiplicationService. However, we didn’t configure it to return any values. We can do this by modifying the declaration slightly:

var multiplicationService = Mock.Of<IMultiplicationService>(s =>
    s.Square(3) == 9 &&
    s.Square(4) == 16);
Enter fullscreen mode Exit fullscreen mode

Here, we’re saying that:

  • When we call Square with 3, we want our mock to return a value 9.

  • When we call Square with 4, we want 16 to be returned.

By doing this, we can be sure that the logic in SumOfSquares is correct, even if the implementation of Square changes such that its results become unreliable. After all, we’re currently writing a unit test: it should only test the logic of a single component, not the overall workflow. After the modification, our test would look like this:

[Test]
public void CalculationServiceCanCalculateSumOfSquares()
{
    // Arrange

    var multiplicationService = Mock.Of<IMultiplicationService>(s =>
        s.Square(3) == 9 &&
        s.Square(4) == 16);

    var service = new CalculationService(multiplicationService);

    // Act

    var result = service.SumOfSquares(3, 4);

    // Assert

    Assert.That(result, Is.EqualTo(25));
}
Enter fullscreen mode Exit fullscreen mode

As shown in Image 1, we should see that our test now passes when we run it.

The Visual Studio Test Explorer showing all tests passed

Image 1: The CalculationServiceCanCalculateSumOfSquares test has passed

Summary

When writing unit tests, you want to test your components in isolation. With components that have dependencies, one way of limiting the scope of logic being tested is by mocking those dependencies. In C#, you can use Moq to do this. When creating a mock, it’s possible to specify its behaviours. In other words, you can configure it to return a specific value whenever it is given a certain input value.

By doing this, you can focus each test on a specific area of the code. With enough unit tests, you’ll be able to build up an overview of your app. And if something goes wrong, your hard work will pay off because your tests will give you a good idea of where the problem lies.


Thanks for reading!

This series aims to cover the basics of software testing, giving you more confidence in your code and how you work with it.

If you found this article useful, please consider signing up for my weekly newsletter for more articles like this delivered straight to your inbox (link goes to Substack).

Top comments (2)

Collapse
 
bytehide profile image
ByteHide

This was an excellent article that highlights the importance of isolating components in tests and mocking dependencies. I really appreciate how you've broken down the process step-by-step, making it easy to follow for beginners and experienced developers alike. Moq seems like a potent tool for crafting isolated tests in C#. Your focus on showing practical examples and offering clear explanations is incredibly valuable.

I have a quick question: When mocking complex dependencies that have many methods and properties, do you have any recommendations for effectively managing these mocks and ensuring that all required behaviors are covered?

Keep up the great work, and I'm looking forward to your future articles on software testing and best practices in C#. Cheers!

Collapse
 
ant_f_dev profile image
Anthony Fung • Edited

Thanks for reading!

Great question. There are two things that you can do.

The first can be helpful sometimes, and is to set a mock’s DefaultValue to DefaultValue.Mock. This will return a mock where possible instead of null if something hasn’t been set up. More details can be found here

The second is something that I do quite regularly. Instead of newing up a mock in the test, I create a factory method to do it for me. The most basic case would look like the example below.

public class Tests
{
    private static MyService CreateMyService(
        IDependency1? dependency1 = null,
        IDependency2? dependency2 = null)
    {
        var service = new MyService(
            dependency1 ?? Mock.Of<IDependency1>(),
            dependency2 ?? Mock.Of<IDependency2>());

        return service;
    }

    [Test]
    public void Test1()
    {
        // ...

        var service = CreateMyService();

        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

If a complex mock of e.g. IDependency2 is needed in the majority of cases, it can be set up in the factory method. This means the setup doesn't have to be repeated each time.

The factory also accepts optional parameters, meaning that if a custom mock is needed for one test, it can be created in that test and passed to the factory.

Using a factory means that each mock is a unique instance, so tests can still be run in parallel if required.

It's quite hard to explain without making this too long (will probably cover it in a future article), but I hope this gives a good idea of what I'm trying to convey.