Valori di ritorno diversi la prima e la seconda volta con Moq


262

Ho un test come questo:

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

GetPageByUrlfunziona due volte nel mio DashboardPathResolver, come posso dire a Moq di tornare nullla prima volta e pageModel.Objectla seconda?

Risposte:


454

Con l'ultima versione di Moq (4.2.1312.1622), è possibile impostare una sequenza di eventi utilizzando SetupSequence . Ecco un esempio:

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

La chiamata a connect avrà esito positivo solo al terzo e al quinto tentativo, altrimenti verrà generata un'eccezione.

Quindi, per il tuo esempio, sarebbe solo qualcosa del tipo:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);

2
Bella risposta, l'unica limitazione è che "SetupSequence" non funziona con i membri protetti.
Chasefornone,

7
Ahimè, SetupSequence()non funziona con Callback(). Se solo lo fosse, si potrebbero verificare le chiamate al metodo deriso in modo "macchina a stati".
urig

@stackunderflow SetupSequencefunziona solo per due chiamate, ma cosa posso fare se ho bisogno di più di due chiamate?
TanvirArjel,

@TanvirArjel, non sono sicuro di cosa intendi ... SetupSequencepuò essere utilizzato per un numero arbitrario di chiamate. Il primo esempio che ho fornito restituisce una sequenza di 5 chiamate.
Stackunderflow

@stackunderflow Siamo spiacenti! Questo è stato il mio fraintendimento! Sì! Hai ragione, funziona come previsto!
TanvirArjel,

115

Le risposte esistenti sono fantastiche, ma ho pensato di aggiungere la mia alternativa che usa System.Collections.Generic.Queuee non richiede alcuna conoscenza speciale del framework beffardo - dal momento che non ne avevo nessuna quando l'ho scritta! :)

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

Poi...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);

Grazie. Ho appena corretto l'errore di battitura in cui stavo accodando il finto pageModel invece di pageModel.Object, quindi ora dovrebbe anche costruire anche! :)
mo.

3
La risposta è corretta, ma nota che questo non funzionerà se vuoi lanciarne una Exceptionperché non puoi Enqueue. Ma SetupSequencefunzionerà (vedi la risposta di @stackunderflow per esempio).
Halvard,

4
Devi usare un metodo delegato per Dequeue. Il modo in cui viene scritto l'esempio restituirà sempre ripetutamente il primo elemento nella coda, poiché il dequeue viene valutato al momento dell'installazione.
Jason Coyne,

7
Questo è un delegato. Se il codice fosse contenuto Dequeue()anziché solo Dequeue, avresti ragione.
mo.

31

L'aggiunta di un callback non ha funzionato per me, ho usato questo approccio invece http://haacked.com/archive/2009/09/29/moq-sequences.aspx e ho finito con un test come questo:

    [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

È possibile utilizzare una richiamata durante l'impostazione dell'oggetto simulato. Dai un'occhiata all'esempio dal 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());

La tua configurazione potrebbe apparire così:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });

1
Ottengo nulla entrambe le volte quando lo faccio: var pageModel = new Mock <IPageModel> (); Modello IPageModel = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (percorso)). Restituisce (() => modello) .Callback (() => {model = pageModel.Object;});
marcus,

GetPageByUrl viene chiamato due volte nel metodo resolver.ResolvePath?
Dan,

ResolvePath contiene il codice seguente ma è comunque nullo entrambe le volte var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
marcus,

2
Confermato che l'approccio callback non funziona (anche provato nella versione precedente di Moq). Un altro possibile approccio, a seconda del test, è semplicemente Setup()ripetere la chiamata e Return()un valore diverso.
Kent Boogaart,


4

Raggiunto qui per lo stesso tipo di problema con requisiti leggermente diversi.
Ho bisogno di ottenere diversi valori di ritorno da finto in base a diversi valori di input e ho trovato una soluzione che IMO è più leggibile in quanto utilizza la sintassi dichiarativa di 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

Per me (Moq 4.13.0 del 2019 qui), ha funzionato anche con il più corto da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., senza It.Is-lambda richiesto affatto.
ojdo,

3

La risposta accettata , così come la risposta SetupSequence , gestisce le costanti di ritorno.

Returns()presenta alcuni utili sovraccarichi in cui è possibile restituire un valore in base ai parametri inviati al metodo deriso. Sulla base della soluzione fornita nella risposta accettata, ecco un altro metodo di estensione per tali sovraccarichi.

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

Sfortunatamente, l'utilizzo del metodo richiede di specificare alcuni parametri del modello, ma il risultato è ancora abbastanza leggibile.

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

Creare sovraccarichi per il metodo di estensione con più parametri ( T2, T3, ecc) se necessario.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.