Advanced Moq Techniques: Methods that Do More than Return Values
Hi Friends.
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");
}
}
}
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:
An
IDataRepository
that will throw an exception when theSaveData
method is called.An
ILogger
that will record arguments provided for theError
method.
To help with (1), let’s create a custom exception for use in our tests:
public class TestingException : Exception
{
}
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"));
}
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.