Restricting mocking by parameter value using Moq


This is something I just recently found out about while using Moq and I thought I would record it here.

When mocking a method using Moq, one can ensure the mock is allowed only when the passed parameters have certain values.

What do I mean by that? Hopefully the following examples will help. In both cases we want the mock to work only if the passed parameter has the value 1885. If any other value is passed to IsValidYear(), the mock should fail (since we are using Mockbehavior.Strict, the mock will throw an exception).

var mockDelorean = new Mock<ITimeMachine>(MockBehavior.Strict);
mockDelorean
  .Setup(d => d.IsValidYear(1885))
  .Returns(true);

or

var targetYear = 1885;
var mockDelorean = new Mock<ITimeMachine>(MockBehavior.Strict);
mockDelorean
  .Setup(d => d.IsValidYear(targetYear))
  .Returns(true);

The problem with both these methods is that value associated with the mocked method is fixed. That may seem obvious in the first example, but it is also true in the second. That is, when the mockDelorean.Setup() statement is evaluated, the mock is restricted to the current value of targetYear and remains associated with that value.

Therefore the following does not work as one might expect:

var targetYear = 1885;
var mockDelorean = new Mock<ITimeMachine>(MockBehavior.Strict);
mockDelorean
  .Setup(d => d.IsValidYear(targetYear))
  .Returns(true);

var testA = mockDelorean.Object.IsValidYear(1885);
targetYear = 2015;
var testB = mockDelorean.Object.IsValidYear(2015);

One might expect this to return true for both testA and testB, since, in each case, the value being passed is equal to the value of targetYear. However, when the mockDelorean.Setup() statement was executed, the value of targetYear was 1885 and that is the value that will be used to check if the call to IsValidYear is allowed.

In the above example, testA will be set to true, but the second call will result in an exception, since there is no mock for IsValidYear() with a parameter value of 2015 and Moq is obeying the MockBehaviour.Strict option.

I was getting around this by using the It.IsAny parameter to allow any values into the mocked method, and then checking the passed parameter in the Returns() evaluation:

var targetYear = 1885;
var mockDelorean = new Mock<ITimeMachine>(MockBehavior.Strict);
mockDelorean
  .Setup(d => d.IsValidYear(It.IsAny<int>())
  .Returns((y) => y == targetYear);

var testA = mockDelorean.Object.IsValidYear(1885);
targetYear = 2015;
var testB = mockDelorean.Object.IsValidYear(2015);

This does run and both testA and TestB are set to true. However it has changed the functionality of the mock. In the previous example, if the year passed was not valid, then an exception was thrown. With the code shown above, no exception is thrown. Instead the call will return false for an invalid year.

For a simple example, like the one we are using, the second case may actually be more useful. However, in more complex unit tests it is often desirable ensure that the mocked methods are always called with only the expected values – that any use of an unexpected value during the test should result in an error.

The way to acheive this is to use an anonymous function as the parameter restriction. Here I will use a lambda expression to restrict the mocking.

var targetYear = 1885;
var mockDelorean = new Mock<ITimeMachine>(MockBehavior.Strict);
mockDelorean
  .Setup(d => d.IsValidYear(It.Is<int>(y => y == targetYear))
  .Returns(true);

var testA = mockDelorean.Object.IsValidYear(1885);
targetYear = 2015;
var testB = mockDelorean.Object.IsValidYear(2015);

The lambda expression is evaluated each time a call is made to the IsValidYear() method, not when the mockDelorean.Setup() statement is evaluated. So the valid year for the method can be changed at run time and the whole thing is still protected from unexpected values by MockBehavior.Strict.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s