Asignación de parámetros / ref en Moq


293

¿Es posible asignar un parámetro out/ refusando Moq (3.0+)?

He analizado el uso Callback(), pero Action<>no admite parámetros de referencia porque está basado en genéricos. También me gustaría preferiblemente poner una restricción ( It.Is) en la entrada del refparámetro, aunque puedo hacerlo en la devolución de llamada.

Sé que Rhino Mocks admite esta funcionalidad, pero el proyecto en el que estoy trabajando ya está usando Moq.


44
Estas preguntas y respuestas tratan sobre Moq 3. Moq 4.8 tiene una compatibilidad mucho mejor para los parámetros de referencia secundaria, que van desde un It.IsAny<T>()comparador similar ( ref It.Ref<T>.IsAny) hasta compatibilidad para la configuración .Callback()y a .Returns()través de un tipo de delegado personalizado que coincide con la firma del método. Los métodos protegidos son igualmente compatibles. Ver, por ejemplo, mi respuesta a continuación .
stakx - ya no contribuye el

Puede usar It.Ref <TValue> .Isany para cualquier método que use parámetros. Por ejemplo: moq.Setup (x => x.Method (out It.Ref <string> .IsAny) .Returns (TValue);
MikBTC

Respuestas:


117

La versión 4.8 de Moq (o posterior) tiene un soporte mucho mejor para los parámetros by-ref:

public interface IGobbler
{
    bool Gobble(ref int amount);
}

delegate void GobbleCallback(ref int amount);     // needed for Callback
delegate bool GobbleReturns(ref int amount);      // needed for Returns

var mock = new Mock<IGobbler>();
mock.Setup(m => m.Gobble(ref It.Ref<int>.IsAny))  // match any value passed by-ref
    .Callback(new GobbleCallback((ref int amount) =>
     {
         if (amount > 0)
         {
             Console.WriteLine("Gobbling...");
             amount -= 1;
         }
     }))
    .Returns(new GobbleReturns((ref int amount) => amount > 0));

int a = 5;
bool gobbleSomeMore = true;
while (gobbleSomeMore)
{
    gobbleSomeMore = mock.Object.Gobble(ref a);
}

El mismo patrón funciona para los outparámetros.

It.Ref<T>.IsAnyTambién funciona para los inparámetros de C # 7 (ya que también son by-ref).


2
Esta es la solución, esto le permite tener cualquier entrada como referencia, exactamente como funcionaría para la entrada sin referencia. De hecho, este es un soporte mejorado muy agradable
grathad el

55
Sin outembargo, esta misma solución no funciona , ¿verdad?
ATD

1
@ATD en parte sí. Declare un delegado con el parámetro out y asigne el valor en la devolución de llamada con la sintaxis de arriba
royalTS

Vale la pena mencionar que si la función que está burlando tiene más argumentos, entonces la firma de devolución de llamada debe seguir el mismo patrón (no solo el parámetro ref / out)
Yoav Feuerstein

320

Para 'fuera', lo siguiente parece funcionar para mí.

public interface IService
{
    void DoSomething(out string a);
}

[TestMethod]
public void Test()
{
    var service = new Mock<IService>();
    var expectedValue = "value";
    service.Setup(s => s.DoSomething(out expectedValue));

    string actualValue;
    service.Object.DoSomething(out actualValue);
    Assert.AreEqual(expectedValue, actualValue);
}

Supongo que Moq analiza el valor de 'esperadaValor' cuando llama a Configuración y lo recuerda.

Para ref, estoy buscando una respuesta también.

La siguiente guía de inicio rápido me pareció útil: https://github.com/Moq/moq4/wiki/Quickstart


77
Creo que el problema que tuve fue que no hay ningún método para asignar los parámetros de salida / referencia del métodoSetup
Richard Szalay

1
No tengo una solución para asignar un parámetro de referencia. Este ejemplo asigna un valor de "valor de salida" a 'b'. Moq no ejecuta la Expresión que pasa a la Configuración, la analiza y se da cuenta de que está proporcionando 'a' para un valor de salida, por lo que analiza el valor actual de 'a' y lo recuerda para las llamadas posteriores.
Craig Celeste

Consulte también los ejemplos de salida y referencia en: code.google.com/p/moq/wiki/QuickStart
TrueWill

99
Esto no funcionará para mí cuando el método de la interfaz simulada se ejecute en un ámbito diferente que tenga su propia variable de salida referenciada (por ejemplo, dentro del método de otra clase). El ejemplo dado anteriormente es conveniente porque la ejecución se produce en el mismo ámbito que la configuración simulada, sin embargo, es demasiado simple para resolver todos los escenarios. El soporte para el manejo explícito del valor out / ref es débil en moq (como dijo alguien más, manejado en el momento de la ejecución).
John K

2
+1: esta es una respuesta útil. Pero: si el tipo de parámetro out es una clase en lugar de un tipo incorporado como una cadena, no creo que esto funcione. Lo probé hoy. El objeto simulado simula la llamada y devuelve un valor nulo a través del parámetro "out".
azheglov

86

EDITAR : en Moq 4.10, ahora puede pasar un delegado que tiene un parámetro de salida o referencia directamente a la función de devolución de llamada:

mock
  .Setup(x=>x.Method(out d))
  .Callback(myDelegate)
  .Returns(...); 

Tendrá que definir un delegado e instanciarlo:

...
.Callback(new MyDelegate((out decimal v)=>v=12m))
...

Para la versión Moq anterior a 4.10:

Avner Kashtan proporciona un método de extensión en su blog que permite configurar el parámetro de salida de una devolución de llamada: parámetros Moq, Callbacks y Out: un caso extremo particularmente complicado

La solución es elegante y hacky. Elegante porque proporciona una sintaxis fluida que se siente como en casa con otras devoluciones de llamada de Moq. Y hacky porque se basa en llamar a algunas API internas de Moq a través de la reflexión.

El método de extensión proporcionado en el enlace anterior no se compiló para mí, por lo que he proporcionado una versión editada a continuación. Deberá crear una firma para cada número de parámetros de entrada que tenga; He proporcionado 0 y 1, pero extenderlo aún más debería ser simple:

public static class MoqExtensions
{
    public delegate void OutAction<TOut>(out TOut outVal);
    public delegate void OutAction<in T1,TOut>(T1 arg1, out TOut outVal);

    public static IReturnsThrows<TMock, TReturn> OutCallback<TMock, TReturn, TOut>(this ICallback<TMock, TReturn> mock, OutAction<TOut> action)
        where TMock : class
    {
        return OutCallbackInternal(mock, action);
    }

    public static IReturnsThrows<TMock, TReturn> OutCallback<TMock, TReturn, T1, TOut>(this ICallback<TMock, TReturn> mock, OutAction<T1, TOut> action)
        where TMock : class
    {
        return OutCallbackInternal(mock, action);
    }

    private static IReturnsThrows<TMock, TReturn> OutCallbackInternal<TMock, TReturn>(ICallback<TMock, TReturn> mock, object action)
        where TMock : class
    {
        mock.GetType()
            .Assembly.GetType("Moq.MethodCall")
            .InvokeMember("SetCallbackWithArguments", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, mock,
                new[] { action });
        return mock as IReturnsThrows<TMock, TReturn>;
    }
}

Con el método de extensión anterior, puede probar una interfaz sin parámetros como:

public interface IParser
{
    bool TryParse(string token, out int value);
}

.. con la siguiente configuración de Moq:

    [TestMethod]
    public void ParserTest()
    {
        Mock<IParser> parserMock = new Mock<IParser>();

        int outVal;
        parserMock
            .Setup(p => p.TryParse("6", out outVal))
            .OutCallback((string t, out int v) => v = 6)
            .Returns(true);

        int actualValue;
        bool ret = parserMock.Object.TryParse("6", out actualValue);

        Assert.IsTrue(ret);
        Assert.AreEqual(6, actualValue);
    }



Editar : para admitir los métodos de retorno de vacío, simplemente necesita agregar nuevos métodos de sobrecarga:

public static ICallbackResult OutCallback<TOut>(this ICallback mock, OutAction<TOut> action)
{
    return OutCallbackInternal(mock, action);
}

public static ICallbackResult OutCallback<T1, TOut>(this ICallback mock, OutAction<T1, TOut> action)
{
    return OutCallbackInternal(mock, action);
}

private static ICallbackResult OutCallbackInternal(ICallback mock, object action)
{
    mock.GetType().Assembly.GetType("Moq.MethodCall")
        .InvokeMember("SetCallbackWithArguments", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, mock, new[] { action });
    return (ICallbackResult)mock;
}

Esto permite probar interfaces como:

public interface IValidationRule
{
    void Validate(string input, out string message);
}

[TestMethod]
public void ValidatorTest()
{
    Mock<IValidationRule> validatorMock = new Mock<IValidationRule>();

    string outMessage;
    validatorMock
        .Setup(v => v.Validate("input", out outMessage))
        .OutCallback((string i, out string m) => m  = "success");

    string actualMessage;
    validatorMock.Object.Validate("input", out actualMessage);

    Assert.AreEqual("success", actualMessage);
}

55
@Wilbert, he actualizado mi respuesta con sobrecargas adicionales para las funciones de retorno de vacío.
Scott Wegner

2
He estado usando esta solución en nuestro conjunto de pruebas y he estado trabajando. Sin embargo, desde la actualización a Moq 4.10, ya no lo hace.
Ristogod

2
Parece que se rompió en este commit github.com/moq/moq4/commit/… . Tal vez hay una mejor manera de hacer esto ahora?
bujía

1
para su información en Moq4, MethodCall ahora es una propiedad de la configuración, por lo que las entrañas de OutCallbackInternal anteriores cambian avar methodCall = mock.GetType().GetProperty("Setup").GetValue(mock); mock.GetType().Assembly.GetType("Moq.MethodCall") .InvokeMember("SetCallbackResponse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, methodCall, new[] { action });
mike mckechnie

1
@Ristogod, con la actualización a Moq 4.10, ahora puede pasar un delegado que tenga un parámetro out o ref directamente a la función de devolución de llamada: mock.Setup(x=>x.Method(out d)).Callback(myDelegate).Returns(...);tendrá que definir un delegado e instanciarlo:...Callback(new MyDelegate((out decimal v)=>v=12m))...;
esteuart

48

Esta es la documentación del sitio de Moq :

// out arguments
var outString = "ack";
// TryParse will return true, and the out argument will return "ack", lazy evaluated
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);


// ref arguments
var instance = new Bar();
// Only matches if the ref argument to the invocation is the same instance
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);

55
Esto es básicamente lo mismo que la respuesta de Parched y tiene la misma limitación, ya que no puede cambiar el valor de salida dependiendo de la entrada ni puede responder a los parámetros de referencia.
Richard Szalay

@ Richard Szalay, puede, pero necesitaría tener configuraciones separadas con parámetros separados "outString"
Sielu

17

Parece que no es posible fuera de la caja. Parece que alguien intentó una solución

Ver esta publicación del foro http://code.google.com/p/moq/issues/detail?id=176

esta pregunta Verifique el valor del parámetro de referencia con Moq


Gracias por la confirmación. En realidad, había encontrado esos dos enlaces en mi búsqueda, pero también noté que Moq enumera una de sus características como "parámetros compatibles de ref / out", por lo que quería estar seguro.
Richard Szalay

3

Para devolver un valor junto con la configuración del parámetro ref, aquí hay un fragmento de código:

public static class MoqExtensions
{
    public static IReturnsResult<TMock> DelegateReturns<TMock, TReturn, T>(this IReturnsThrows<TMock, TReturn> mock, T func) where T : class
        where TMock : class
    {
        mock.GetType().Assembly.GetType("Moq.MethodCallReturn`2").MakeGenericType(typeof(TMock), typeof(TReturn))
            .InvokeMember("SetReturnDelegate", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, mock,
                new[] { func });
        return (IReturnsResult<TMock>)mock;
    }
}

A continuación, declare su propio delegado que coincida con la firma del método a burlar y proporcione su propia implementación del método.

public delegate int MyMethodDelegate(int x, ref int y);

    [TestMethod]
    public void TestSomething()
    {
        //Arrange
        var mock = new Mock<ISomeInterface>();
        var y = 0;
        mock.Setup(m => m.MyMethod(It.IsAny<int>(), ref y))
        .DelegateReturns((MyMethodDelegate)((int x, ref int y)=>
         {
            y = 1;
            return 2;
         }));
    }

¿Funciona esto cuando no tienes acceso a la variable que se va a pasar como y? Tengo una función que lleva dos argumentos de referencia a DayOfWeek. Necesito configurar ambos para un día en particular en el código auxiliar falso y el tercer argumento es un contexto falso de base de datos. Pero el método delegado simplemente no se llama. Parece que Moq esperaría coincidir con el local que se está pasando a su función "MyMethod". ¿Es así como funciona para tu ejemplo? Gracias.
Greg Veres

3

Sobre la base de Billy Jakes Awnser, hice un método simulado completamente dinámico con un parámetro out. Estoy publicando esto aquí para cualquiera que lo encuentre útil (probablemente solo futuro).

// Define a delegate with the params of the method that returns void.
delegate void methodDelegate(int x, out string output);

// Define a variable to store the return value.
bool returnValue;

// Mock the method: 
// Do all logic in .Callback and store the return value.
// Then return the return value in the .Returns
mockHighlighter.Setup(h => h.SomeMethod(It.IsAny<int>(), out It.Ref<int>.IsAny))
  .Callback(new methodDelegate((int x, out int output) =>
  {
    // do some logic to set the output and return value.
    output = ...
    returnValue = ...
  }))
  .Returns(() => returnValue);

2

Estoy seguro de que la solución de Scott funcionó en un punto,

Pero es un buen argumento para no usar la reflexión para mirar las API privadas. Está roto ahora.

Pude establecer parámetros usando un delegado

      delegate void MockOutDelegate(string s, out int value);

    public void SomeMethod()
    {
        ....

         int value;
         myMock.Setup(x => x.TryDoSomething(It.IsAny<string>(), out value))
            .Callback(new MockOutDelegate((string s, out int output) => output = userId))
            .Returns(true);
    }

1

Esto puede ser una solución.

[Test]
public void TestForOutParameterInMoq()
{
  //Arrange
  _mockParameterManager= new Mock<IParameterManager>();

  Mock<IParameter > mockParameter= new Mock<IParameter >();
  //Parameter affectation should be useless but is not. It's really used by Moq 
  IParameter parameter= mockParameter.Object;

  //Mock method used in UpperParameterManager
  _mockParameterManager.Setup(x => x.OutMethod(out parameter));

  //Act with the real instance
  _UpperParameterManager.UpperOutMethod(out parameter);

  //Assert that method used on the out parameter of inner out method are really called
  mockParameter.Verify(x => x.FunctionCalledInOutMethodAfterInnerOutMethod(),Times.Once());

}

1
Esto es básicamente lo mismo que la respuesta de Parched y tiene la misma limitación, ya que no puede cambiar el valor de salida dependiendo de la entrada ni puede responder a los parámetros de referencia.
Richard Szalay

1

Luché con muchas de las sugerencias aquí antes de crear una instancia de una nueva clase 'Fake' que implemente cualquier interfaz que esté tratando de burlarse. Luego, simplemente puede establecer el valor del parámetro out con el método mismo.


0

Luché con esto durante una hora esta tarde y no pude encontrar una respuesta en ninguna parte. Después de jugar por mi cuenta, pude encontrar una solución que funcionó para mí.

string firstOutParam = "first out parameter string";
string secondOutParam = 100;
mock.SetupAllProperties();
mock.Setup(m=>m.Method(out firstOutParam, out secondOutParam)).Returns(value);

La clave aquí es mock.SetupAllProperties();que eliminará todas las propiedades por usted. Esto puede no funcionar en todos los casos de prueba, pero si todo lo que importa es conseguir que el return valuede YourMethodentonces este trabajo fino voluntad.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.