Как настроить метод дважды для разных параметров с помощью Moq

Я хотел бы установить метод с Moq дважды, но кажется, что последний переопределяет предыдущие. Вот моя первоначальная настройка:

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

Это работает нормально, но я хочу, чтобы это возвращало new ValidUserContext() если имя пользователя или пароль отличается от переменных имени username и password как указано выше. Для этого я добавил еще одну настройку, но на этот раз она переопределяет вышеуказанную и всегда применяет ее:

membershipServiceMock.Setup(ms =>
    ms.ValidateUser(It.IsAny<string>(), It.IsAny<string>())
).Returns(
    new ValidUserContext()
);

Какой самый элегантный способ справиться с подобной ситуацией в Moq?

редактировать

Я решил проблему с помощью следующего подхода, но я думаю, что есть лучший способ справиться с этим:

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

Ответ 1

Moq поддерживает это из коробки с ограничениями аргументов:

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 также работает, но порядок важен:

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

Ответ 2

Другой готовый вариант - использовать версию Return <> для возврата различных ValidUserContexts в зависимости от параметров. Это не лучше, чем приведенный выше ответ, просто еще один вариант.

Мы настроили ValidateUser(), чтобы он возвращал результат функции GetUserContext (string, string), передавая имя пользователя и пароль, с которыми был вызван ValidateUser().

[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;

    }
}

Ответ 3

Если вы посмотрите на определение функции для 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);

Все, что вам нужно сделать, это переключить порядок двух вызовов Setup():

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

таким образом, если входные данные действительно являются username и password, оба вызова Setup() квалифицированы, но последний выигрывает из-за правила, и когда у вас есть какие-либо другие входные данные, только первый из них сопоставляется и применяется.