Valores de retorno diferentes na primeira e na segunda vez com Moq

262

Eu tenho um teste como este:

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

GetPageByUrlexecuta duas vezes no meu DashboardPathResolver, como posso dizer ao Moq para retornar nullna primeira e pageModel.Objectna segunda?

Marcus
fonte

Respostas:

454

Com a versão mais recente do Moq (4.2.1312.1622), você pode configurar uma sequência de eventos usando o SetupSequence . Aqui está um exemplo:

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

A conexão de chamada só será bem-sucedida na terceira e quinta tentativa, caso contrário, uma exceção será lançada.

Portanto, para o seu exemplo, seria algo como:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
fonte
2
Boa resposta, a única limitação é que o "SetupSequence" não funciona com membros protegidos.
Chasefornone 26/03
7
Infelizmente, SetupSequence()não funciona com Callback(). Se isso acontecesse, era possível verificar as chamadas para o método simulado de uma maneira "máquina de estado".
urig
O @stackunderflow SetupSequencefunciona apenas para duas chamadas, mas o que posso fazer se precisar de mais de duas chamadas?
TanvirArjel
@TanvirArjel, não sei o que você quer dizer ... SetupSequencepode ser usado para um número arbitrário de chamadas. O primeiro exemplo que eu dei retorna uma sequência de 5 chamadas.
stackunderflow
@stackunderflow Desculpe! Este foi o meu mal-entendido! Sim! Você está correto, trabalhando como esperado!
TanvirArjel 9/01/19
115

As respostas existentes são ótimas, mas pensei em lançar minha alternativa que apenas usa System.Collections.Generic.Queuee não requer nenhum conhecimento especial da estrutura de zombaria - já que eu não tinha nenhuma quando a escrevi! :)

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

Então...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mo.
fonte
Obrigado. Eu apenas corrigi o erro de digitação em que eu estava enfileirando a simulação pageModel em vez de pageModel.Object, então agora ele deve ser compilado também! :)
mo.
3
A resposta está correta, mas observe que isso não funcionará se você quiser jogar um Exceptioncomo não pode Enqueue. Mas SetupSequencefuncionará (veja a resposta do @stackunderflow, por exemplo).
achou
4
Você precisa usar um método delegado para a remoção da fila. A maneira como a amostra é gravada sempre retornará o primeiro item na fila repetidamente, porque a desenfileiramento é avaliada no momento da instalação.
Jason Coyne
7
Esse é um delegado. Se o código contido em Dequeue()vez de apenas Dequeue, você estaria correto.
mo.
31

A adição de um retorno de chamada não funcionou para mim. Em vez disso, usei essa abordagem http://haacked.com/archive/2009/09/29/moq-sequences.aspx e terminei com um teste como este:

    [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
fonte
29

Você pode usar um retorno de chamada ao configurar seu objeto simulado. Veja o exemplo do 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());

Sua configuração pode ficar assim:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
fonte
1
Eu fico nulo ao fazer isso: var pageModel = new Mock <IPageModel> (); Modelo IPageModel = nulo; repository.Setup (x => x.GetPageByUrl <IPageModel> (caminho)). Retorna (() => modelo) .Callback (() => {model = pageModel.Object;});
Marcus
GetPageByUrl é chamado duas vezes no método resolver.ResolvePath?
Dan
ResolvePath contém o código abaixo, mas ainda é nulo nas duas vezes var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
Marcus
2
Confirmado que a abordagem de retorno de chamada não funciona (mesmo tentada na versão Moq anterior). Outra abordagem possível - dependendo do seu teste - é apenas fazer a Setup()ligação novamente e com Return()um valor diferente.
21813 Kent Kentogaoga
4

Chegou aqui para o mesmo tipo de problema com requisitos ligeiramente diferentes.
Preciso obter diferentes valores de retorno do mock com base em diferentes valores de entrada e encontrar uma solução que IMO seja mais legível, pois usa a sintaxe declarativa do 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
fonte
Para mim (Moq 4.13.0 de 2019 aqui), funcionou mesmo com o menor da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., sem It.Is-ambda necessário.
ojdo 6/09/19
3

A resposta aceita , bem como a resposta SetupSequence , lida com as constantes retornadas.

Returns()possui algumas sobrecargas úteis em que você pode retornar um valor com base nos parâmetros que foram enviados para o método simulado. Com base na solução dada na resposta aceita, aqui está outro método de extensão para essas sobrecargas.

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

Infelizmente, o uso do método requer que você especifique alguns parâmetros do modelo, mas o resultado ainda é bastante legível.

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

Criar sobrecargas para o método de extensão com vários parâmetros ( T2, T3, etc.), se necessário.

Torbjörn Kalin
fonte