Différentes valeurs de retour la première et la deuxième fois avec Moq

262

J'ai un test comme celui-ci:

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

GetPageByUrls'exécute deux fois dans mon DashboardPathResolver, comment dire à Moq de revenir nullla première fois et pageModel.Objectla seconde?

marcus
la source

Réponses:

454

Avec la dernière version de Moq (4.2.1312.1622), vous pouvez configurer une séquence d'événements à l'aide de SetupSequence . Voici un exemple:

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

L'appel de connexion ne réussira que lors des troisième et cinquième tentatives, sinon une exception sera levée.

Donc, pour votre exemple, ce serait juste quelque chose comme:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
la source
2
Bonne réponse, la seule limitation est que "SetupSequence" ne fonctionne pas avec les membres protégés.
Chasefornone
7
Hélas, SetupSequence()ne fonctionne pas avec Callback(). Si c'était le cas, on pourrait vérifier les appels à la méthode simulée à la manière d'une "machine d'état".
urig
@stackunderflow SetupSequencene fonctionne que pour deux appels, mais que puis-je faire si j'ai besoin de plus de deux appels?
TanvirArjel
@TanvirArjel, vous ne savez pas ce que vous voulez dire ... SetupSequencepeut être utilisé pour un nombre arbitraire d'appels. Le premier exemple que j'ai donné renvoie une séquence de 5 appels.
stackunderflow
@stackunderflow Désolé! C'était mon malentendu! Oui! Vous avez raison de travailler comme prévu!
TanvirArjel
115

Les réponses existantes sont excellentes, mais je pensais que je mettrais mon alternative qui utilise simplement System.Collections.Generic.Queueet ne nécessite aucune connaissance particulière du cadre de simulation - car je n'en avais pas quand je l'ai écrit! :)

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

Ensuite...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mo.
la source
Merci. Je viens de corriger la faute de frappe où je mettais en file d'attente la maquette de pageModel au lieu de pageModel.Object, alors maintenant, il devrait même être construit aussi! :)
mo.
3
La réponse est correcte, mais notez que cela ne fonctionnera pas si vous voulez lancer un Exceptioncomme vous ne pouvez pas Enqueue. Mais SetupSequencecela fonctionnera (voir la réponse de @stackunderflow par exemple).
Halvard
4
Vous devez utiliser une méthode déléguée pour la mise en file d'attente. La façon dont l'échantillon est écrit renvoie toujours le premier élément de la file d'attente à plusieurs reprises, car la file d'attente est évaluée au moment de la configuration.
Jason Coyne
7
C'est un délégué. Si le code contenait Dequeue()au lieu de juste Dequeue, vous auriez raison.
mo.
31

L'ajout d'un rappel n'a pas fonctionné pour moi, j'ai plutôt utilisé cette approche http://haacked.com/archive/2009/09/29/moq-sequences.aspx et je me suis retrouvé avec un test comme celui-ci:

    [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
la source
29

Vous pouvez utiliser un rappel lors de la configuration de votre faux objet. Jetez un œil à l'exemple du Wiki Moq ( 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());

Votre configuration pourrait ressembler à ceci:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
la source
1
J'obtiens null les deux fois quand je fais ceci: var pageModel = new Mock <IPageModel> (); Modèle IPageModel = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (path)). Renvoie (() => model) .Callback (() => {model = pageModel.Object;});
marcus
GetPageByUrl est-il appelé deux fois dans la méthode resolver.ResolvePath?
Dan
ResolvePath contient le code ci-dessous mais il est toujours nul les deux fois var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
marcus
2
Confirmé que l'approche de rappel ne fonctionne pas (même essayée dans la version antérieure de Moq). Une autre approche possible - en fonction de votre test - est de simplement refaire l' Setup()appel, et Return()une valeur différente.
Kent Boogaart
4

Atteint ici pour le même type de problème avec des exigences légèrement différentes.
J'ai besoin d'obtenir différentes valeurs de retour de mock basées sur différentes valeurs d'entrée et trouvé une solution qui IMO est plus lisible car elle utilise la syntaxe déclarative de 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
la source
Pour moi (Moq 4.13.0 à partir de 2019 ici), cela a fonctionné même avec le plus court da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., aucun It.Is-lambda requis du tout.
ojdo
3

La réponse acceptée , ainsi que la réponse SetupSequence , gère le retour des constantes.

Returns()contient des surcharges utiles où vous pouvez renvoyer une valeur basée sur les paramètres qui ont été envoyés à la méthode simulée. Basé sur la solution donnée dans la réponse acceptée, voici une autre méthode d'extension pour ces surcharges.

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

Malheureusement, l'utilisation de la méthode vous oblige à spécifier certains paramètres de modèle, mais le résultat est encore assez lisible.

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

Créer des surcharges pour la méthode d'extension avec plusieurs paramètres ( T2, T3, etc.) , si nécessaire.

Torbjörn Kalin
la source