Diferentes valores de retorno la primera y la segunda vez con Moq

262

Tengo una prueba como esta:

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

GetPageByUrlfunciona dos veces en mi DashboardPathResolver, ¿cómo puedo decirle a Moq que regrese nullla primera y pageModel.Objectla segunda?

marcus
fuente

Respuestas:

454

Con la última versión de Moq (4.2.1312.1622), puede configurar una secuencia de eventos utilizando SetupSequence . Aquí hay un ejemplo:

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

Llamar a connect solo tendrá éxito en el tercer y quinto intento; de lo contrario, se lanzará una excepción.

Entonces, para su ejemplo, sería algo así como:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
fuente
2
Buena respuesta, la única limitación es que "SetupSequence" no funciona con miembros protegidos.
Chasefornone
77
Por desgracia, SetupSequence()no funciona con Callback(). Si solo lo hiciera, uno podría verificar las llamadas al método simulado en una forma de "máquina de estado".
urig
@stackunderflow SetupSequencesolo funciona para dos llamadas, pero ¿qué puedo hacer si necesito más de dos llamadas?
TanvirArjel
@TanvirArjel, no estoy seguro de lo que quieres decir ... SetupSequencese puede usar para un número arbitrario de llamadas. El primer ejemplo que di devuelve una secuencia de 5 llamadas.
stackunderflow
@stackunderflow ¡Lo siento! Este fue mi malentendido! ¡Si! ¡Estás en lo correcto trabajando como se esperaba!
TanvirArjel
115

Las respuestas existentes son geniales, pero pensé en agregar mi alternativa que solo usa System.Collections.Generic.Queuey no requiere ningún conocimiento especial del marco de trabajo burlón, ¡ya que no tenía ninguno cuando lo escribí! :)

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

Luego...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mes.
fuente
Gracias. Acabo de corregir el error tipográfico en el que estaba poniendo en cola el simulacro de pageModel en lugar de pageModel.Object, ¡así que ahora incluso debería construirse también! :)
mes
3
La respuesta es correcta, pero tenga en cuenta que esto no funcionará si desea lanzar un Exceptioncomo no puede Enqueue. Pero SetupSequencefuncionará (vea la respuesta de @stackunderflow, por ejemplo).
Halvard
44
Debe usar un método delegado para la Dequeue. La forma en que se escribe la muestra siempre devolverá el primer elemento de la cola repetidamente, porque la cola se evalúa en el momento de la configuración.
Jason Coyne
77
Eso es un delegado. Si el código contenía en Dequeue()lugar de solo Dequeue, estaría correcto.
mes
31

Agregar una devolución de llamada no funcionó para mí, utilicé este enfoque en su lugar http://haacked.com/archive/2009/09/29/moq-sequences.aspx y terminé con una prueba como esta:

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

Puede usar una devolución de llamada al configurar su objeto simulado. Eche un vistazo al ejemplo de 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());

Su configuración podría verse así:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
fuente
1
Me vuelvo nulo las dos veces cuando hago esto: var pageModel = new Mock <IPageModel> (); Modelo de IPageModel = nulo; repository.Setup (x => x.GetPageByUrl <IPageModel> (ruta)). Devuelve (() => modelo) .Callback (() => {model = pageModel.Object;});
marcus
¿Se llama a GetPageByUrl dos veces dentro del método resolver.ResolvePath?
Dan
ResolvePath contiene el siguiente código, pero sigue siendo nulo en ambas ocasiones var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
marcus
2
Confirmó que el enfoque de devolución de llamada no funciona (incluso lo intentó en la versión anterior de Moq). Otro enfoque posible, dependiendo de su prueba, es Setup()volver a hacer la llamada y obtener Return()un valor diferente.
Kent Boogaart
4

Accedido aquí por el mismo tipo de problema con requisitos ligeramente diferentes.
Necesito obtener diferentes valores de retorno de simulacros basados ​​en diferentes valores de entrada y encontré una solución que IMO es más legible ya que usa la sintaxis declarativa 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
fuente
Para mí (Moq 4.13.0 de 2019 aquí), funcionó incluso con el más corto da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., sin It.Is-lambda requerido.
ojdo
3

La respuesta aceptada , así como la respuesta SetupSequence , maneja las constantes de retorno.

Returns()tiene algunas sobrecargas útiles donde puede devolver un valor basado en los parámetros que se enviaron al método simulado. Basado en la solución dada en la respuesta aceptada, aquí hay otro método de extensión para esas 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));
    }
}

Desafortunadamente, el uso del método requiere que especifique algunos parámetros de plantilla, pero el resultado sigue siendo bastante legible.

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

Crear sobrecargas para el método de extensión con múltiples parámetros ( T2, T3, etc.) si es necesario.

Torbjörn Kalin
fuente