How to set up a method twice for different parameters with Moq
C#.NetUnit TestingMoqC# Problem Overview
I would like to set up a method with Moq twice but it seems that the last one overrides the previous ones. Here's my initial setup:
string username = "foo";
string password = "bar";
var principal = new GenericPrincipal(
new GenericIdentity(username),
new[] { "Admin" });
var membershipServiceMock = new Mock<IMembershipService>();
membershipServiceMock.Setup(ms =>
ms.ValidateUser(username, password)
).Returns(new ValidUserContext {
Principal = principal
});
This works out fine but I want this to return new ValidUserContext()
if the username or password is different to the username
and password
variables as above. To do that, I added another setup but this time it overrides the above one and always applies it:
membershipServiceMock.Setup(ms =>
ms.ValidateUser(It.IsAny<string>(), It.IsAny<string>())
).Returns(
new ValidUserContext()
);
What is the most elegant way of handling this type of situation with Moq?
Edit
I solved the problem with the below approach but I guess there is a better way of handling this:
var membershipServiceMock = new Mock<IMembershipService>();
membershipServiceMock.Setup(ms =>
ms.ValidateUser(It.IsAny<string>(), It.IsAny<string>())
).Returns<string, string>((u, p) =>
(u == username && p == password) ?
new ValidUserContext {
Principal = principal
}
: new ValidUserContext()
);
C# Solutions
Solution 1 - C#
Moq supports this out of box with argument constraints:
mock.Setup(ms => ms.ValidateUser(
It.Is<string>(u => u == username), It.Is<string>(p => p == password))
.Returns(new ValidUserContext { Principal = principal });
mock.Setup(ms => ms.ValidateUser(
It.Is<string>(u => u != username), It.Is<string>(p => p != password))
.Returns(new ValidUserContext());
Catch-all It.IsAny
also works, but the order is important:
// general constraint first so that it doesn't overwrite more specific ones
mock.Setup(ms => ms.ValidateUser(
It.IsAny<string>(), It.IsAny<string>())
.Returns(new ValidUserContext());
mock.Setup(ms => ms.ValidateUser(
It.Is<string>(u => u == username), It.Is<string>(p => p == password))
.Returns(new ValidUserContext { Principal = principal });
Solution 2 - C#
If you look at the function definition for Setup()
:
// Remarks:
// If more than one setup is specified for the same method or property, the latest
// one wins and is the one that will be executed.
public ISetup<T, TResult> Setup<TResult>(Expression<Func<T, TResult>> expression);
All you need to do is switch the order of the two Setup()
calls:
membershipServiceMock.Setup(ms =>
ms.ValidateUser(It.IsAny<string>(), It.IsAny<string>())
).Returns(
new ValidUserContext()
);
membershipServiceMock.Setup(ms =>
ms.ValidateUser(username, password)
).Returns(new ValidUserContext {
Principal = principal
});
so if the input is indeed username
and password
, both Setup()
calls are qualified but the later one wins because of the rule and when you have any other inputs, only the first one is matched and applied.
Solution 3 - C#
Another out-of-the-box option is to use the Return<> version to return different ValidUserContexts depending upon the parameters. It is not better than the above answer, just another option.
We set up ValidateUser() to return the result of a function GetUserContext(string, string), passing in the username and password with which ValidateUser() was called.
[TestClass]
public class MultipleReturnValues {
public class ValidUserContext {
public string Principal { get; set; }
}
public interface IMembershipService {
ValidUserContext ValidateUser(string name, string password);
}
[TestMethod]
public void DifferentPricipals() {
var mock = new Mock<IMembershipService>();
mock.Setup(mk => mk.ValidateUser(It.IsAny<string>(), It.IsAny<string>())).Returns<string, string>(GetUserContext);
var validUserContext = mock.Object.ValidateUser("abc", "cde");
Assert.IsNull(validUserContext.Principal);
validUserContext = mock.Object.ValidateUser("foo", "bar");
Assert.AreEqual(sPrincipal, validUserContext.Principal);
}
private static string sPrincipal = "A Principal";
private static ValidUserContext GetUserContext(string name, string password) {
var ret = new ValidUserContext();
if (name == "foo" && password == "bar") {
ret = new ValidUserContext { Principal = sPrincipal };
}
return ret;
}
}