Різні значення повернення в перший і другий раз з 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другий?

Відповіді:


452

З останньою версією 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);

2
Приємна відповідь, єдине обмеження - "SetupSequence" не працює із захищеними членами.
Chasefornone

7
На жаль, SetupSequence()не працює Callback(). Якби це робилося, можна було б перевірити виклики до методу глузування «державною машиною».
уріг

@stackunderflow SetupSequenceпрацює лише для двох дзвінків, але що я можу зробити, якщо потрібно більше двох дзвінків?
TanvirArjel

@TanvirArjel, не впевнений, що ти маєш на увазі ... SetupSequenceможе використовуватися для довільної кількості дзвінків. Перший я приклад повертає послідовність з 5 дзвінків.
стік потоку

@stackunderflow Вибачте! Це було моє непорозуміння! Так! Ви правильно виправляєте це, як очікувалося!
ТанвірАржель

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-sequences.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 модель = 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()інше значення.
Kent Boogaart


4

Тут зверталися до тієї самої проблеми з дещо різними вимогами.
Мені потрібно отримати різні повернені значення від макету на основі різних вхідних значень та знайдене рішення, яке IMO більш читабельне, оскільки воно використовує декларативний синтаксис Moq (linkq до 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

Для мене (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і т.д.) , якщо необхідно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.