When you’re using Moq to set up a mocked method, you can use Callback() to capture the parameters passed into the mocked method:
string capturedJson;
mockRepo.Setup(t => t.Save(It.IsAny<string>()))
.Callback((string json) =>
{
Console.WriteLine("Repository.Save(json) called. Captured json parameter");
capturedJson = json;
});
//assert against the captured JSON later
Code language: C# (cs)
There are two main use cases for capturing parameters in a test:
- Logging method calls for troubleshooting.
- Simplifying assertions involving complex parameters.
In this article, I’ll show examples of using Callback() in those two scenarios, and then I’ll explain some problems to watch out for when you’re trying to set up a Callback() lambda.
Table of Contents
Example – Use Callback() to log method calls for troubleshooting
You can use Callback() to log method calls and their parameters, which can help with troubleshooting.
For example, let’s say you have a failing unit test and you can’t figure out why it’s failing. So you put in a Callback() to log the calls.
//arrange
var mockRepo = new Mock<IMessageRepository>();
var messageService = new MessageService(mockRepo.Object);
mockRepo.Setup(t => t.Get(10))
.Returns(() => "{\"Id\":10, \"Text\":\"Test\"}")
.Callback((int id) =>
{
//Log call for troubleshooting
Console.WriteLine($"Repo.Get({id}) called");
});
//act
var message = messageService.ProcessMessage(100);
//assert
Assert.IsNotNull(message);
Code language: C# (cs)
This isn’t logging anything, which tells you the mocked method isn’t getting called at all. You can see ProcessMessage(id) is calling Repository.Get(id).
Can you spot the problem in the test? The mocked method is setup for Get(10), whereas you’re calling ProcessMessage(100), which is why the mocked method isn’t intercepting the call at all (and hence why it’s not invoking the Callback() lambda). This is just a typo.
After fixing the problem, the test passes and outputs the following:
Repo.Get(10) called
Code language: plaintext (plaintext)
Using the parameterless Callback() overload
You aren’t required to pass in the parameters to the Callback() lambda. You can use the parameterless overload of Callback() if you want:
mockRepo.Setup(t => t.Get(10))
.Returns(() => "{\"Id\":10, \"Text\":\"Test\"}")
.Callback(() =>
{
Console.WriteLine($"Repo.Get() called");
});
Code language: C# (cs)
This is a simpler option than passing in the parameters and avoids errors you can run into when trying to set up the Callback() lambda correctly.
Example – Use Callback() to simplify assertions involving the captured parameters
When you need to assert against parameters passed into mocked methods in complex scenarios, you can use Callback() to capture the parameters, and then assert against the parameters directly.
Here’s an example. This captures a JSON string, deserializes it, and asserts against the deserialized object:
//arrange
var mockRepo = new Mock<IMessageRepository>();
var messageService = new MessageService(mockRepo.Object);
Message capturedMessage = null;
mockRepo.Setup(t => t.Save(It.IsAny<string>()))
.Callback((string json) =>
{
//Capture parameter for assertion later
capturedMessage = JsonSerializer.Deserialize<Message>(json);
});
//act
messageService.Send(new Message() { SendAt = DateTimeOffset.Now.AddMinutes(1) });
//Assert against captured parameter
Assert.IsTrue(capturedMessage.SendAt > DateTimeOffset.Now);
Code language: C# (cs)
In very simple scenarios, you can stick with the Verify() + It.Is<T>() approach. But for anything non-trivial, using this Callback() approach can simplify things significantly. I’ll explain why it simplifies things below.
Why capturing the parameters simplifies assertions
To see why capturing the parameters simplifies the assertions, let’s take a look at an alternative approach using Verify() + It.Is<T>().
Just like the example above, this will assert that the JSON parameter passed to Repository.Save(json) has a future date (by comparing against the current date and time). We have to use Verify() and It.Is<T>() together to try to examine the passed in parameter:
mockRepo.Verify(t => t.Save(It.Is<string>(json =>
{
var message = JsonSerializer.Deserialize<Message>(json);
return message.SendAt > DateTimeOffset.Now
};
Code language: C# (cs)
First, this is harder to read compared with the simplified assertion we were able to do with the Callback() approach. Second, this results in the following compile-time error:
CS0834 A lambda expression with a statement body cannot be converted to an expression tree
We can’t use a statement body (curly braces with multiple executable lines in it) here. Instead, we have to use the following one-liner:
mockRepo.Verify(t => t.Save(It.Is<string>(json => JsonSerializer.Deserialize<Message>(json, null).SendAt > DateTimeOffset.Now)));
Code language: C# (cs)
First, this is even harder to read. Second, notice we had to pass in null to Deserialize() even though it’s an optional parameter. This is because optional parameters aren’t optional when using the Moq API (due to it using System.Linq.Expressions).
As the scenario becomes more complex, this one-liner approach becomes seriously complicated.
This shows how using Callback() to capture parameters can greatly simplify assertions.
Callback lambda parameters must match the mocked method parameters
If the Callback lambda parameters don’t match the mocked method parameters, you get the following runtime exception:
System.ArgumentException: Invalid callback. Setup on method with parameters cannot invoke callback with parameters
Note: This doesn’t apply to the parameterless Callback() overload. It only applies to the myriad Callback(Action) overloads.
For example, let’s say you’re mocking IRepository and want to set up a callback on the Delete(int, bool) method:
public interface IRepository
{
public void Delete(int id, bool cascadingDelete=true);
}
Code language: C# (cs)
Here’s an example of an incorrect callback:
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Delete(It.IsAny<int>(), It.IsAny<bool>()))
.Callback((int id) =>
{
Console.WriteLine($"Delete called with {id}");
});
Code language: C# (cs)
This would throw the following exception:
System.ArgumentException: Invalid callback. Setup on method with parameters (int, bool) cannot invoke callback with parameters (int)
As the exception mentions, it expects the lambda parameters to match the Delete(int, bool) parameters. They must be the same type, in the same order, and even include optional parameters (notice that bool cascadingDelete is an optional parameter).
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Delete(It.IsAny<int>(), It.IsAny<bool>()))
.Callback((int id, bool cascadingDelete) =>
{
Console.WriteLine($"Delete(id={id}, cascadingDelete={cascadingDelete})");
});
Code language: C# (cs)
Callback lambda parameter types must be specified explicitly
If you don’t specify the callback lambda parameter types explicitly, then you’ll get the following compile-time error:
Error CS1660 Cannot convert lambda expression to type ‘InvocationAction’ because it is not a delegate type
This is referring to this Callback() overload in the Moq API, which the compiler thinks you’re trying to use:
ICallbackResult Callback(InvocationAction action);
Code language: C# (cs)
For example, let’s say you’re mocking IRepository and want to set up a callback on the Save(bool) method:
public interface IRepository
{
public void Save(bool inTransaction=false);
}
Code language: C# (cs)
The following callback setup is incorrect because it’s not specifying the type for inTransaction parameter. This results in the CS1660 compile-time error:
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Save(It.IsAny<bool>()))
.Callback((inTransaction) =>
{
Console.WriteLine($"Save({inTransaction})");
});
Code language: C# (cs)
You must specify the parameter type explicitly. You can either specify the type in the lambda declaration, like this:
.Callback((bool inTransaction) =>
{
Console.WriteLine($"Save({inTransaction})");
});
Code language: C# (cs)
Or you can declare the generic type parameter, like this:
.Callback<bool>((inTransaction) =>
{
Console.WriteLine($"Save({inTransaction})");
});
Code language: C# (cs)
The first approach is better because it keeps the parameter type and name together, which is easier to read. Choose whichever option you prefer though.