Pregunta Al burlarse de una clase con Moq, ¿cómo puedo usar CallBase para solo métodos específicos?


Realmente aprecio los de Moq Loose comportamiento de burla que devuelve valores predeterminados cuando no se establecen expectativas. Es conveniente y me ahorra código, y también actúa como una medida de seguridad: las dependencias no se llamarán involuntariamente durante la prueba de la unidad (siempre que sean virtuales).

Sin embargo, estoy confundido acerca de cómo mantener estos beneficios cuando el método bajo prueba pasa a ser virtual.
En este caso yo hacer quiere llamar al código real para ese único método, mientras sigue teniendo el resto de la clase sin burla.

Todo lo que encontré en mi búsqueda es que podía establecer mock.CallBase = true para asegurar que el método sea llamado. Sin embargo, eso afecta a toda la clase. No quiero hacer eso porque me pone en un dilema sobre todas las otras propiedades y métodos de la clase que ocultan las dependencias de llamadas: si CallBase es verdadero, entonces tengo que

  1. Los apéndices de instalación de todas las propiedades y métodos que ocultan las dependencias: aunque mi prueba no crea que deba preocuparse por esas dependencias, o
  2. Espero que no me olvide de configurar ningún stubs (y que no se agreguen nuevas dependencias al código en el futuro) - Pruebas de unidad de riesgo que golpean una dependencia real.

Lo que creo que quiero es algo así como:
mock.Setup(m => m.VirtualMethod()).CallBase();
para que cuando llame mock.Object.VirtualMethod(), Moq llama a la implementación real ...

P: Con Moq, ¿hay alguna manera de probar un método virtual, cuando me burlé de la clase para poner algunas pocas dependencias? Es decir. Sin recurrir a CallBase = true y tener que colgar todas las dependencias?


Código de ejemplo para ilustrar
(usa MSTest, InternalsVisibleTo DynamicProxyGenAssembly2)

En el siguiente ejemplo, TestNonVirtualMethod pasa, pero TestVirtualMethod falla - devuelve nulo.

public class Foo
{
    public string NonVirtualMethod() { return GetDependencyA(); }
    public virtual string VirtualMethod() { return GetDependencyA();}

    internal virtual string GetDependencyA() { return "! Hit REAL Dependency A !"; }
    // [... Possibly many other dependencies ...]
    internal virtual string GetDependencyN() { return "! Hit REAL Dependency N !"; }
}

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestNonVirtualMethod()
    {
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);

        string result = mockFoo.Object.NonVirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    }

    [TestMethod]
    public void TestVirtualMethod() // Fails
    {
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);
        // (I don't want to setup GetDependencyB ... GetDependencyN here)

        string result = mockFoo.Object.VirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    }

    string expectedResultString = "Hit mock dependency A - OK";
}

32
2018-05-12 21:39


origen


Respuestas:


Creo que la respuesta de Lunivore fue correcta en el momento en que fue escrita.

En las versiones más nuevas de Moq (creo que desde la versión 4.1 de 2013), es posible hacer lo que quiera con la sintaxis exacta que proponga. Es decir:

mock.Setup(m => m.VirtualMethod()).CallBase();

Esto configura el simulacro libre para llamar a la implementación base de VirtualMethod en lugar de solo regresar default(WhatEver), pero para este miembro (VirtualMethod) solamente.


Como las notas del usuario BornToCode en los comentarios, esto no funcionará si el método tiene tipo de retorno void. Cuando el VirtualMethod no es nulo, el Setup llamada da una Moq.Language.Flow.ISetup<TMock, TResult> que hereda el CallBase() método de Moq.Language.Flow.IReturns<TMock, TResult>. Pero cuando el método es nulo, obtenemos un Moq.Language.Flow.ISetup<TMock> en cambio, que carece de la deseada CallBase() método.


9
2017-08-12 14:37



Como nadie ha respondido esta pregunta durante años y creo que merece una respuesta, me centraré en la pregunta de mayor nivel que me hizo: cómo mantener estos beneficios cuando el método bajo prueba es virtual.

Respuesta rápida: no puedes hacerlo con Moq, o al menos no directamente. Pero puedes hacerlo.

Digamos que tiene dos aspectos de comportamiento, donde el aspecto A es virtual y el aspecto B no. Eso más o menos refleja lo que tienes en tu clase. B puede usar otros métodos o no; tu decides.

Por el momento, tu clase Foo está haciendo dos cosas, ambas A y B. Puedo decir que son responsabilidades separadas solo porque quiere burlarse de A y probar B por sí mismo.

En lugar de tratar de burlarse del método virtual sin burlarse de nada más, puede:

  • mover el comportamiento A a una clase separada
  • la dependencia inyecta la nueva clase con A en Foo mediante Fooconstructor
  • invocar esa clase de B.

Ahora puede simular A, y aún llamar al código real para B..N sin invocar realmente el A. real. Puede mantener A virtual o acceder a él a través de una interfaz y simular eso. Esto está en línea con el Principio de Responsabilidad Individual, también.

Puedes crear constructores en cascada Foo - hacer el constructor Foo() llama al constructor Foo(new A()) - por lo que ni siquiera necesita un marco de inyección de dependencia para hacer esto.

¡Espero que esto ayude!


8
2017-10-18 16:51



Ahí es una forma de llamar al método real y tener una devolución de llamada cuando el método es void, pero es De Verdad hacky. Tienes que devolver la llamada, llámalo explícitamente y engaña a Moq para que llame al real.

Por ejemplo, dada esta clase

public class MyInt
{
    public bool CalledBlah { get; private set; }

    public virtual void Blah()
    {
        this.CalledBlah = true;
    }
}

Puedes escribir tu prueba de esta manera:

[Test]
public void Test_MyRealBlah()
{
    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    bool calledBlah = false;
    m.When(() => !calledBlah)
        .Setup(i => i.Blah())
        .Callback(() => { calledBlah = true; m.Object.Blah(); })
        .Verifiable();

    m.Object.Blah();

    Assert.IsTrue(m.Object.CalledBlah);
    m.VerifyAll();
}

El aspecto clave es que rastreas si se ha llamado a la versión falsa, y luego configuras el simulacro para no llamar al falso si ya ha sido llamado.

Todavía puedes hacer algo similar si tomas argumentos y el valor importa:

public class MyInt
{
    public List<int> CalledBlahArgs { get; private set; }

    public MyInt()
    {
        this.CalledBlahArgs = new List<int>();
    }

    public virtual void Blah(int a)
    {
        this.CalledBlahArgs.Add(a);
    }
}

[Test]
public void Test_UpdateQueuedGroups_testmockcallback()
{
    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    List<int> fakeBlahArgs = new List<int>();

    m.Setup(i => i.Blah(It.Is<int>(a => !fakeBlahArgs.Contains(a))))
        .Callback<int>((a) => { fakeBlahArgs.Add(a); m.Object.Blah(a); })
        .Verifiable();

    m.Object.Blah(1);

    Assert.AreEqual(1, m.Object.CalledBlahArgs.Single());
    m.Verify(b => b.Blah(It.Is<int>(id => 1 == id)));
}

-1
2017-07-22 08:33