How to set up a method twice for different parameters with Moq

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()
);
52619 次浏览

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 });

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;


}
}

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.