DEV Community

Cover image for Advanced Moq Techniques: Methods that Do More than Return Values
Anthony Fung
Anthony Fung

Posted on • Originally published at webdeveloperdiary.substack.com

Advanced Moq Techniques: Methods that Do More than Return Values

In last week’s part of the series, we looked at the two ways that we can set up testing mocks using Moq.

  • We can do this quickly and succinctly with the newer Linq to Mocks syntax, or…

  • We can use a fluent syntax that offers greater control.

We noted that it was advantageous to use Linq to Mocks for simpler mocks, and that we should opt for the fluent syntax where fine-tuning a mock’s behaviour is necessary. In this part, let’s look at another reason to use the fluent syntax.

Functional Programming vs Using State

Writing code with a functional programming mindset is a great practice. By avoiding use of state, code becomes easier to:

  • Run in parallel (where necessary). As there is no shared state, one thread cannot influence another’s results.

  • Reason with. Pure functions are idempotent: the output will always be the same for a given set of inputs.

  • Write tests for. You have an output to assert against.

All are important from a testability point of view. But there are times when writing functionally isn’t feasible/possible. When we need to write void methods, we should have tests for these too.

Imagine writing an app that includes a data service. This (currently) has one method: SaveData. Our code so far follows:

public interface ILogger
{
    public void Error(string message);
}

public interface IDataRepository
{
    public void Save(string data);
}

public class DataService
{
    private readonly IDataRepository _repository;
    private readonly ILogger _logger;

    public DataService(IDataRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public void SaveData(string data)
    {
        try
        {
            _repository.Save(data);
        }

        catch (Exception)
        {
            _logger.Error("An error occurred");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We’ve wrapped the line _repository.Save(data) in a try/catch statement – we want to log an error if a problem arises while committing data. When unit-testing this behaviour, we want to set up mocks for each of the data service’s two dependencies. We need:

  1. An IDataRepository that will throw an exception when the SaveData method is called.

  2. An ILogger that will record arguments provided for the Error method.

To help with (1), let’s create a custom exception for use in our tests:

public class TestingException : Exception
{
}
Enter fullscreen mode Exit fullscreen mode

We’ve previously seen how to set up mocks to return values. But here, we need our mocks to throw exceptions and remember data passed to them.

So how do we set these up?

Setting up Void Methods

In addition to setting up mocks that behave functionally – i.e. return values based on their inputs – Moq’s fluent syntax has some other methods. Along with Returns, we also have:

  • Throws, which causes a mock to throw an exception of the specified type.

  • Callback. This runs logic without returning data.

With these two methods, we can write the following test:

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

    var repository = new Mock<IDataRepository>();
    var logger = new Mock<ILogger>();
    var logs = new List<string>();

    repository
        .Setup(r => r.Save(It.IsAny<string>()))
        .Throws<TestingException>();

    logger
        .Setup(l => l.Error(It.IsAny<string>()))
        .Callback<string>(msg => logs.Add(msg));

    var service = new DataService(repository.Object, logger.Object);

    // Act

    service.SaveData("Save");

    // Assert

    Assert.That(logs.Count, Is.EqualTo(1));
    Assert.That(logs[0], Is.EqualTo("An error occurred"));
}
Enter fullscreen mode Exit fullscreen mode

Here we’ve used Throws to set up our IDataRepository to throw a custom TestingException when Save is called with any argument. We’ve also set up our logger with Callback to do something when Error is called: it adds the provided string to the logs list so that we can inspect it during the assertion phase of the test.

We could also verify our mocks instead of running callback logic to add to logs. However, it’s simpler to see what’s going on this way. In any case, we’ll revisit verifying mocks in another part.

Summary

We previously looked at how to set up mocks with Moq to return values based on their input arguments: writing code with a functional programming mindset can be beneficial and makes writing tests easier. However, we often write functional and void methods in C#. When testing, mocks can be set up to throw exceptions and run callbacks in addition to returning values.

Having looked at an example, you now understand potential use cases for these behaviours and how to set them up. As with many things in programming, there are many ways to achieve the same objectives sometimes. And while mocks keep records of methods called and arguments provided, you have an alternative way that can be easier to work with.


Thanks for reading!

Software development evolves quickly. But we can all help each other learn. By sharing, we all grow together.

I write stories, tips, and insights as an Angular/C# developer.

If you’d like to never miss an issue, you’re invited to subscribe for free for more articles like this, delivered straight to your inbox (link goes to Substack).

Top comments (0)