Różne wartości zwracane za pierwszym i drugim razem z Moq

262

Mam taki test:

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

GetPageByUrldziała dwa razy DashboardPathResolver, jak mogę powiedzieć Moq, aby wróciła nullpo raz pierwszy i pageModel.Objectdrugi?

Marek
źródło

Odpowiedzi:

452

W najnowszej wersji Moq (4.2.1312.1622) możesz ustawić sekwencję zdarzeń za pomocą SetupSequence . Oto przykład:

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

Wywołanie połączenia zakończy się powodzeniem tylko przy trzeciej i piątej próbie, w przeciwnym razie zostanie zgłoszony wyjątek.

W twoim przykładzie byłoby to po prostu coś takiego:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
źródło
2
Dobra odpowiedź, jedynym ograniczeniem jest to, że „SetupSequence” nie działa z chronionymi elementami.
Chasefornone
7
Niestety, SetupSequence()nie działa z Callback(). Gdyby tak się stało, można by zweryfikować wywołania wyśmiewanej metody w sposób „automatyczny”.
urig
@stackunderflow SetupSequencedziała tylko dla dwóch połączeń, ale co mogę zrobić, jeśli potrzebujesz więcej niż dwóch połączeń?
TanvirArjel
@TanvirArjel, nie jestem pewien, co masz na myśli ... SetupSequencemożna użyć do dowolnej liczby połączeń. Pierwszy przykład, który podałem, zwraca sekwencję 5 połączeń.
stackunderflow
@stackunderflow Przepraszamy! To było moje nieporozumienie! Tak! Masz rację, działa zgodnie z oczekiwaniami!
TanvirArjel
115

Istniejące odpowiedzi są świetne, ale pomyślałem, że rzuciłbym swoją alternatywę, która po prostu wykorzystuje System.Collections.Generic.Queuei nie wymaga żadnej specjalnej wiedzy na temat kpiącego frameworka - ponieważ nie miałem żadnej, kiedy to napisałem! :)

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

Następnie...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mo.
źródło
Dzięki. Właśnie poprawiłem literówkę, w której umieszczałem kolejkę makiety pageModel zamiast pageModel.Object, więc teraz powinno się nawet kompilować! :)
mo.
3
Odpowiedź jest prawidłowa, ale pamiętaj, że to nie zadziała, jeśli chcesz rzucić, Exceptionbo nie możesz Enqueue. Ale SetupSequencezadziała (patrz na przykład odpowiedź @stackunderflow).
Halvard
4
Musisz użyć delegowanej metody dla Dequeue. Sposób, w jaki zapisana jest próbka, zawsze zwraca wielokrotnie pierwszy element w kolejce, ponieważ kolejka jest oceniana w czasie instalacji.
Jason Coyne
7
To jest delegat. Jeśli kod zawiera Dequeue()zamiast po prostu Dequeue, będziesz poprawny.
mo.
31

Dodanie wywołania zwrotnego nie działało dla mnie, zamiast tego zastosowałem to podejście http://haacked.com/archive/2009/09/29/moq-sequences.aspx i skończyłem z takim testem:

    [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);
    }
Marek
źródło
29

Podczas konfigurowania próbnego obiektu można użyć wywołania zwrotnego. Spójrz na przykład z 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());

Twoja konfiguracja może wyglądać następująco:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
źródło
1
Za każdym razem otrzymuję zero: var pageModel = new Mock <IPageModel> (); Model IPageModel = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (ścieżka)). Zwraca (() => model). Callback (() => {model = pageModel.Object;});
marcus
Czy metoda GetPageByUrl jest wywoływana dwukrotnie w ramach metody resolver.ResolvePath?
Dan
ResolvePath zawiera poniższy kod, ale nadal jest null za każdym razem var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
marcus
2
Potwierdzono, że metoda wywołania zwrotnego nie działa (nawet próbowano we wcześniejszej wersji Moq). Innym możliwym podejściem - w zależności od testu - jest Setup()ponowne wykonanie połączenia i Return()podanie innej wartości.
Kent Boogaart
4

Osiągnięto tutaj ten sam problem z nieco innymi wymaganiami.
Muszę uzyskać różne zwracane wartości od próbnego opartego na różnych wartościach wejściowych i znaleźć rozwiązanie, które IMO jest bardziej czytelne, ponieważ używa składni deklaratywnej 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
źródło
Dla mnie (Moq 4.13.0 z 2019 r. Tutaj) działało nawet z krótszą da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., nie It.Iswymagającą-lambda w ogóle.
ojdo
3

Odpowiedź akceptowana , jak również odpowiedź SetupSequence , uchwyty powrocie stałe.

Returns()ma pewne przydatne przeciążenia, w których można zwrócić wartość na podstawie parametrów wysłanych do próbnej metody. W oparciu o rozwiązanie podane w zaakceptowanej odpowiedzi, oto kolejna metoda rozszerzenia dla tych przeciążeń.

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

Niestety użycie tej metody wymaga określenia niektórych parametrów szablonu, ale wynik jest nadal dość czytelny.

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

Tworzenie przeciążeń dla metodę rozszerzenia z wieloma parametrami ( T2, T3itp), w razie potrzeby.

Torbjörn Kalin
źródło