Различные возвращаемые значения в первый и второй раз с Moq

262

У меня есть такой тест:

    [TestCase("~/page/myaction")]
    public void Page_With_Custom_Action(string path) {
        // Arrange
        var pathData = new Mock<IPathData>();
        var pageModel = new Mock<IPageModel>();
        var repository = new Mock<IPageRepository>();
        var mapper = new Mock<IControllerMapper>();
        var container = new Mock<IContainer>();

        container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);

        repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageModel.Object);

        pathData.Setup(x => x.Action).Returns("myaction");
        pathData.Setup(x => x.Controller).Returns("page");

        var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

        // Act
        var data = resolver.ResolvePath(path);

        // Assert
        Assert.NotNull(data);
        Assert.AreEqual("myaction", data.Action);
        Assert.AreEqual("page", data.Controller);
    }

GetPageByUrlработает дважды в моем DashboardPathResolver, как я могу сказать Moq вернуть nullпервый раз и pageModel.Objectвторой?

Маркус
источник

Ответы:

454

В последней версии Moq (4.2.1312.1622) вы можете настроить последовательность событий, используя SetupSequence . Вот пример:

_mockClient.SetupSequence(m => m.Connect(It.IsAny<String>(), It.IsAny<int>(), It.IsAny<int>()))
        .Throws(new SocketException())
        .Throws(new SocketException())
        .Returns(true)
        .Throws(new SocketException())
        .Returns(true);

Вызов соединения будет успешным только с третьей и пятой попытки, в противном случае будет сгенерировано исключение.

Так что для вашего примера это будет что-то вроде:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
источник
2
Хороший ответ, единственным ограничением является то, что «SetupSequence» не работает с защищенными членами.
Chasefornone
7
Увы, SetupSequence()не работает с Callback(). Если бы только это было так, можно было бы проверить вызовы в методе «конечного автомата».
Urig
@stackunderflow SetupSequenceработает только для двух вызовов, но что я могу сделать, если нужно более двух вызовов?
TanvirArjel
@TanvirArjel, не уверен, что вы имеете в виду ... SetupSequenceможет быть использовано для произвольного количества вызовов. Первый пример, который я привел, возвращает последовательность из 5 вызовов.
stackunderflow
@stackunderflow Извините! Это было мое неправильное понимание! Да! Вы правильно работаете, как и ожидалось!
TanvirArjel
115

Существующие ответы великолепны, но я подумал, что добавлю свою альтернативу, которая просто использует System.Collections.Generic.Queueи не требует каких-либо специальных знаний о фальшивом фреймворке - поскольку у меня их не было, когда я его писал! :)

var pageModel = new Mock<IPageModel>();
IPageModel pageModelNull = null;
var pageModels = new Queue<IPageModel>();
pageModels.Enqueue(pageModelNull);
pageModels.Enqueue(pageModel.Object);

Затем...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
денежный перевод по почте
источник
Спасибо. Я просто исправил опечатку, в которой я ставил макет pageModel вместо pageModel.Object, так что теперь он должен даже собираться! :)
мес.
3
Ответ правильный, но учтите, что это не сработает, если вы хотите бросить, Exceptionпоскольку вы не можете этого Enqueueсделать. Но SetupSequenceбудет работать (см. Ответ от @stackunderflow, например).
Халвард
4
Вы должны использовать делегированный метод для Dequeue. При написании примера он всегда будет возвращать первый элемент в очереди несколько раз, поскольку очередь оценивается во время настройки.
Джейсон Койн
7
Это делегат. Если бы код содержался Dequeue()вместо просто Dequeue, вы были бы правы.
мес.
31

Добавление обратного вызова не работает для меня, я использовал этот подход вместо этого http://haacked.com/archive/2009/09/29/moq-sequence.aspx, и я закончил с таким тестом:

    [TestCase("~/page/myaction")]
    [TestCase("~/page/myaction/")]
    public void Page_With_Custom_Action(string virtualUrl) {

        // Arrange
        var pathData = new Mock<IPathData>();
        var pageModel = new Mock<IPageModel>();
        var repository = new Mock<IPageRepository>();
        var mapper = new Mock<IControllerMapper>();
        var container = new Mock<IContainer>();

        container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);
        repository.Setup(x => x.GetPageByUrl<IPageModel>(virtualUrl)).ReturnsInOrder(null, pageModel.Object);

        pathData.Setup(x => x.Action).Returns("myaction");
        pathData.Setup(x => x.Controller).Returns("page");

        var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

        // Act
        var data = resolver.ResolvePath(virtualUrl);

        // Assert
        Assert.NotNull(data);
        Assert.AreEqual("myaction", data.Action);
        Assert.AreEqual("page", data.Controller);
    }
Маркус
источник
29

Вы можете использовать обратный вызов при настройке вашего фиктивного объекта. Взгляните на пример из Moq Wiki ( http://code.google.com/p/moq/wiki/QuickStart ).

// returning different values on each invocation
var mock = new Mock<IFoo>();
var calls = 0;
mock.Setup(foo => foo.GetCountThing())
    .Returns(() => calls)
    .Callback(() => calls++);
// returns 0 on first invocation, 1 on the next, and so on
Console.WriteLine(mock.Object.GetCountThing());

Ваша установка может выглядеть так:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Дэн
источник
1
Я получаю нулевое значение оба раза, когда я делаю это: var pageModel = new Mock <IPageModel> (); IPageModel model = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (путь)). Возвращает (() => модель) .Callback (() => {model = pageModel.Object;});
Marcus
GetPageByUrl вызывается дважды в методе resolver.ResolvePath?
Дан
ResolvePath содержит приведенный ниже код, но он все равно равен нулю: var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
Marcus
2
Подтверждено, что метод обратного вызова не работает (даже пробовал в более ранней версии Moq). Другой возможный подход - в зависимости от вашего теста - это просто Setup()повторить вызов и Return()использовать другое значение.
Кент Boogaart
4

Достигнуто здесь для той же проблемы с немного другим требованием.
Мне нужно получить разные возвращаемые значения из mock на основе разных входных значений и найти решение, которое IMO более читабельно, так как использует декларативный синтаксис Moq (linq to Mocks).

public interface IDataAccess
{
   DbValue GetFromDb(int accountId);  
}

var dataAccessMock = Mock.Of<IDataAccess>
(da => da.GetFromDb(It.Is<int>(acctId => acctId == 0)) == new Account { AccountStatus = AccountStatus.None }
&& da.GetFromDb(It.Is<int>(acctId => acctId == 1)) == new DbValue { AccountStatus = AccountStatus.InActive }
&& da.GetFromDb(It.Is<int>(acctId => acctId == 2)) == new DbValue { AccountStatus = AccountStatus.Deleted });

var result1 = dataAccessMock.GetFromDb(0); // returns DbValue of "None" AccountStatus
var result2 = dataAccessMock.GetFromDb(1); // returns DbValue of "InActive"   AccountStatus
var result3 = dataAccessMock.GetFromDb(2); // returns DbValue of "Deleted" AccountStatus
Saravanan
источник
Для меня (Moq 4.13.0 от 2019 здесь), он работал даже с более коротким da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., без- It.Isлямбда вообще не требуется.
ojdo
3

Общепринятый ответ , а также SetupSequence ответ , ручки возвращая константы.

Returns()имеет несколько полезных перегрузок, где вы можете вернуть значение на основе параметров, которые были отправлены в проверяемый метод. Основываясь на решении, приведенном в принятом ответе, вот еще один метод расширения для этих перегрузок.

public static class MoqExtensions
{
    public static IReturnsResult<TMock> ReturnsInOrder<TMock, TResult, T1>(this ISetup<TMock, TResult> setup, params Func<T1, TResult>[] valueFunctions)
        where TMock : class
    {
        var queue = new Queue<Func<T1, TResult>>(valueFunctions);
        return setup.Returns<T1>(arg => queue.Dequeue()(arg));
    }
}

К сожалению, использование метода требует указания некоторых параметров шаблона, но результат все еще вполне читабелен.

repository
    .Setup(x => x.GetPageByUrl<IPageModel>(path))
    .ReturnsInOrder(new Func<string, IPageModel>[]
        {
            p => null, // Here, the return value can depend on the path parameter
            p => pageModel.Object,
        });

Создание перегрузок для метода расширения с несколькими параметрами ( T2, T3и т.д.) , если необходимо.

Торбьерн Калин
источник