Unterschiedliche Rückgabewerte beim ersten und zweiten Mal mit Moq

262

Ich habe einen Test wie diesen:

    [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 wird zweimal in meinem Dashboardpathresolver ausgeführt. Wie kann ich Moq anweisen, beim ersten Mal null und beim zweiten Mal pageModel.Ojbect zurückzugeben?

Marcus
quelle

Antworten:

452

Mit der neuesten Version von Moq (4.2.1312.1622) können Sie mit SetupSequence eine Folge von Ereignissen einrichten . Hier ist ein Beispiel:

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

Das Aufrufen von connect ist nur beim dritten und fünften Versuch erfolgreich, andernfalls wird eine Ausnahme ausgelöst.

Für Ihr Beispiel wäre es also nur so etwas wie:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
Stackunderflow
quelle
2
Gute Antwort, die einzige Einschränkung ist, dass "SetupSequence" nicht mit geschützten Mitgliedern funktioniert.
Chasefornone
7
Leider SetupSequence()funktioniert nicht mit Callback(). Wenn dies nur der Fall wäre, könnte man Aufrufe der verspotteten Methode auf eine "Zustandsmaschine" -Methode überprüfen.
Urig
@stackunderflow SetupSequencefunktioniert nur für zwei Anrufe, aber was kann ich tun, wenn mehr als zwei Anrufe erforderlich sind?
TanvirArjel
@ TanvirArjel, nicht sicher, was Sie meinen ... SetupSequencekann für eine beliebige Anzahl von Anrufen verwendet werden. Das erste Beispiel, das ich gegeben habe, gibt eine Folge von 5 Anrufen zurück.
Stackunderflow
@stackunderflow Sorry! Das war mein Missverständnis! Ja! Sie haben Recht, dass es wie erwartet funktioniert!
TanvirArjel
115

Die vorhandenen Antworten sind großartig, aber ich dachte, ich würde meine Alternative einwerfen, die nur System.Collections.Generic.Queuespezielle Kenntnisse des Spott-Frameworks verwendet und keine erfordert - da ich zum Zeitpunkt des Schreibens keine hatte! :) :)

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

Dann...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mo.
quelle
Vielen Dank. Ich habe gerade den Tippfehler behoben, bei dem ich den pageModel-Mock anstelle von pageModel.Object in die Warteschlange gestellt habe. Jetzt sollte er sogar erstellt werden! :)
mo.
3
Die Antwort ist richtig, aber beachten Sie, dass dies nicht funktioniert, wenn Sie eine werfen möchten, Exceptionda Sie es nicht Enqueuekönnen. Funktioniert aber SetupSequence(siehe Antwort von @stackunderflow zum Beispiel).
Halvard
4
Sie müssen eine delegierte Methode für die Warteschlange verwenden. Bei der Art und Weise, wie das Beispiel geschrieben wird, wird immer das erste Element in der Warteschlange wiederholt zurückgegeben, da die Warteschlange zum Zeitpunkt der Einrichtung ausgewertet wird.
Jason Coyne
7
Das ist ein Delegierter. Wenn der Code Dequeue()statt nur enthalten Dequeuewäre, wären Sie korrekt.
mo.
31

Das Hinzufügen eines Rückrufs hat bei mir nicht funktioniert. Stattdessen habe ich diesen Ansatz verwendet: http://haacked.com/archive/2009/09/29/moq-sequences.aspx. Am Ende hatte ich einen Test wie den folgenden:

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

Sie können einen Rückruf verwenden, wenn Sie Ihr Scheinobjekt einrichten. Schauen Sie sich das Beispiel aus dem Moq-Wiki an ( 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());

Ihr Setup könnte folgendermaßen aussehen:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
quelle
1
Ich bekomme beide Male null, wenn ich das mache: var pageModel = new Mock <IPageModel> (); IPageModel model = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (Pfad)). Rückgabe (() => Modell) .Callback (() => {model = pageModel.Object;});
Marcus
Wird GetPageByUrl innerhalb der resolver.ResolvePath-Methode zweimal aufgerufen?
Dan
ResolvePath enthält den folgenden Code, ist jedoch beide Male immer noch null. Var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
Marcus
2
Bestätigt, dass der Rückrufansatz nicht funktioniert (auch in früheren Moq-Versionen versucht). Ein anderer möglicher Ansatz - abhängig von Ihrem Test - besteht darin, den Setup()Anruf erneut zu tätigen und Return()einen anderen Wert zu verwenden.
Kent Boogaart
4

Hier für die gleiche Art von Problem mit etwas anderen Anforderungen erreicht.
Ich muss verschiedene Rückgabewerte von mock erhalten, die auf unterschiedlichen Eingabewerten basieren, und eine Lösung gefunden haben, die IMO besser lesbar ist, da sie die deklarative Syntax von Moq (linq to Mocks) verwendet.

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
quelle
Für mich (Moq 4.13.0 von 2019 hier) funktionierte es sogar mit dem kürzeren da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., überhaupt nicht It.Isbenötigten Lambda.
ojdo
3

Die akzeptierte Antwort sowie die SetupSequence-Antwort behandeln die Rückgabe von Konstanten.

Returns()hat einige nützliche Überladungen, bei denen Sie einen Wert zurückgeben können, der auf den Parametern basiert, die an die verspottete Methode gesendet wurden. Basierend auf der in der akzeptierten Antwort angegebenen Lösung ist hier eine weitere Erweiterungsmethode für diese Überlastungen.

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

Leider müssen Sie für die Verwendung der Methode einige Vorlagenparameter angeben, aber das Ergebnis ist immer noch gut lesbar.

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

Erstellen Überlastungen für die Erweiterungsmethode mit mehreren Parametern ( T2, T3, usw.) , wenn nötig.

Torbjörn Kalin
quelle