La mejor manera de unir los métodos de prueba que llaman a otros métodos dentro de la misma clase

35

Recientemente estuve discutiendo con algunos amigos cuál de los siguientes 2 métodos es el mejor para resguardar los resultados devueltos o las llamadas a métodos dentro de la misma clase de los métodos dentro de la misma clase.

Este es un ejemplo muy simplificado. En realidad, las funciones son mucho más complejas.

Ejemplo:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Entonces, para probar esto, tenemos 2 métodos.

Método 1: utilice funciones y acciones para reemplazar la funcionalidad de los métodos. Ejemplo:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Método 2: Haga que los miembros sean virtuales, la clase derivada y en la clase derivada use Funciones y Acciones para reemplazar la funcionalidad Ejemplo:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Quiero saber cuál es mejor y también ¿POR QUÉ?

Actualización: NOTA: La función B también puede ser pública

tranceru1
fuente
Su ejemplo es simple, pero no exactamente correcto. FunctionAdevuelve un bool pero solo establece una variable local xy no devuelve nada.
Eric P.
1
En este ejemplo particular, FunctionB puede ser public staticpero en una clase diferente.
Para la revisión de código, se espera que publique código real, no una versión simplificada del mismo. Ver las preguntas frecuentes. Tal como está, está haciendo una pregunta específica que no busca una revisión de código.
Winston Ewert
1
FunctionBestá roto por diseño. new Random().Next()Casi siempre está mal. Debe inyectar la instancia de Random. ( Randomtambién es una clase mal diseñada, que puede causar algunos problemas adicionales)
CodesInChaos
DI en un plano más general a través de los delegados es absolutamente bien en mi humilde opinión
jk.

Respuestas:

32

Editado después de la actualización original del póster.

Descargo de responsabilidad: no es un programador de C # (principalmente Java o Ruby). Mi respuesta sería: no lo probaría en absoluto, y no creo que debas hacerlo.

La versión más larga es: los métodos privados / protegidos no son parte de la API, son básicamente opciones de implementación, que puede decidir revisar, actualizar o descartar por completo sin ningún impacto en el exterior.

Supongo que tiene una prueba en FunctionA (), que es la parte de la clase que es visible desde el mundo externo. Debería ser el único que tiene un contrato para implementar (y que podría probarse). Su método privado / protegido no tiene contrato para cumplir y / o probar.

Vea una discusión relacionada allí: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Después del comentario , si FunctionB es público, simplemente probaré ambos utilizando la prueba unitaria. Puede pensar que la prueba de FunctionA no es totalmente "unidad" (como se llama FunctionB), pero eso no me preocuparía demasiado: si la prueba de FunctionB funciona pero no la prueba de FunctionA, significa claramente que el problema no está en el subdominio de FunctionB, que es lo suficientemente bueno para mí como discriminador.

Si realmente desea poder separar por completo las dos pruebas, usaría algún tipo de técnica de burla para burlarse de FunctionB cuando pruebe FunctionA (generalmente, devuelve un valor correcto conocido y fijo). Me falta el conocimiento del ecosistema C # para aconsejar una biblioteca de burlas específica, pero puede consultar esta pregunta .

Martín
fuente
2
Completamente de acuerdo con la respuesta de @Martin. Cuando escribe pruebas unitarias para la clase, no debe probar los métodos . Lo que está probando es un comportamiento de clase , que el contrato (la declaración de lo que se supone que debe hacer la clase) se cumple. Por lo tanto, sus pruebas unitarias deben cubrir todos los requisitos expuestos para esta clase (utilizando métodos / propiedades públicas), incluidos casos excepcionales
Hola, gracias por la respuesta, pero no respondió mi pregunta. No importa si FunctionB era privado / protegido. También puede ser público y aún se puede llamar desde FunctionA.
La forma más común de manejar este problema, sin rediseñar la clase base, es subclasificar MyClassy anular el método con la funcionalidad que desea stub. También podría ser una buena idea actualizar su pregunta para incluirla que FunctionBpodría ser pública.
Eric P.
1
protectedLos métodos son parte de la superficie pública de una clase, a menos que se asegure de que no puede haber implementaciones de su clase en diferentes ensamblados.
CodesInChaos
2
El hecho de que FunctionA llame a FunctionB es un detalle irrelevante desde una perspectiva de prueba de unidad. Si las pruebas para FunctionA se escriben correctamente, se trata de un detalle de implementación que podría reestructurarse más adelante sin interrumpir las pruebas (siempre y cuando el comportamiento general de FunctionA no se modifique). El verdadero problema es que la recuperación de FunctionB de un número aleatorio debe hacerse con un objeto inyectado, de modo que pueda usar un simulacro durante la prueba para asegurarse de que se devuelva un número conocido. Esto le permite probar entradas / salidas bien conocidas.
Dan Lyons
11

Me suscribo a la teoría de que si una función es importante para probar, o es importante para reemplazar, es lo suficientemente importante como para no ser un detalle de implementación privada de la clase bajo prueba, sino ser un detalle de implementación pública de una clase diferente .

Entonces, si estoy en un escenario donde tengo

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Entonces voy a refactorizar.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Ahora tengo un escenario en el que D () es independientemente comprobable y completamente reemplazable.

Como medio de organización, mi colaborador podría no vivir en el mismo nivel de espacio de nombres. Por ejemplo, si Aestá en FooCorp.BLL, entonces mi colaborador podría tener otra capa más profunda, como en FooCorp.BLL.Collaborators (o cualquier nombre que sea apropiado). Es posible que mi colaborador solo sea visible dentro del ensamblaje a través del internalmodificador de acceso, que luego expondría a mis proyectos de prueba de unidad a través del InternalsVisibleToatributo de ensamblaje. La conclusión es que aún puede mantener su API limpia, en lo que respecta a las personas que llaman, mientras produce un código verificable.

Anthony Pegram
fuente
Sí, si ICollaborator necesita varios métodos. Sin embargo, si tiene un objeto cuyo único trabajo es envolver un método único , prefiero verlo reemplazado por un delegado.
jk.
Tendría que decidir si un delegado nombrado tiene sentido o si una interfaz tiene sentido, y yo no decidiré por usted. Personalmente, no soy reacio a las clases de método único (público). Cuanto más pequeño, mejor, ya que cada vez son más fáciles de entender.
Anthony Pegram
0

Agregando a lo que Martin señala,

Si su método es privado / protegido, no lo pruebe. Es interno de la clase y no se debe acceder fuera de la clase.

En ambos enfoques que mencionas, tengo estas preocupaciones:

Método 1: esto realmente cambia el comportamiento de la clase bajo prueba en la prueba.

Método 2: en realidad, esto no prueba el código de producción, sino que prueba otra implementación.

En el problema indicado, veo que la única lógica de A es ver si la salida de FunctionB es par. Aunque es ilustrativo, FunctionB proporciona un valor aleatorio, que es difícil de probar.

Espero un escenario realista en el que podamos configurar MyClass de modo que sepamos qué FunctionB devolvería. Entonces se conoce nuestro resultado esperado, podemos llamar a FunctionA y afirmar el resultado real.

Srikanth Venugopalan
fuente
3
protectedes casi lo mismo que public. Solo privatey internalson detalles de implementación.
CodesInChaos
@codeinchaos - Tengo curiosidad aquí. Para una prueba, los métodos protegidos son 'privados' a menos que modifique los atributos del ensamblaje. Solo los tipos derivados tienen acceso a los miembros protegidos. Con la excepción de virtual, no veo por qué una protección debería tratarse de forma similar a pública. ¿podrías elaborar por favor?
Srikanth Venugopalan
Dado que esas clases derivadas pueden estar en diferentes ensamblados, están expuestas a código de terceros y, por lo tanto, son parte de la superficie pública de su clase. Para probarlos, puede hacerlos internal protected, usar un asistente de reflexión privado o crear una clase derivada en su proyecto de prueba.
CodesInChaos
@CodesInChaos, acordó que la clase derivada puede estar en diferentes ensamblados, pero el alcance aún está restringido a los tipos base y derivados. Modificar el modificador de acceso solo para que sea comprobable es algo por lo que estoy un poco nervioso. Lo he hecho, pero parece ser un antipatrón para mí.
Srikanth Venugopalan
0

Personalmente uso el Método 1, es decir, convertir todos los métodos en Acciones o Funciones, ya que esto me ha mejorado enormemente la capacidad de prueba del código. Como con cualquier solución, hay ventajas y desventajas con este enfoque:

Pros

  1. Permite una estructura de código simple donde el uso de patrones de código solo para pruebas unitarias puede agregar complejidad adicional.
  2. Permite sellar las clases y eliminar los métodos virtuales que requieren los marcos de simulación populares como Moq. El sellado de clases y la eliminación de métodos virtuales los convierten en candidatos para la optimización en línea y otras compilaciones. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Simplifica la capacidad de prueba, ya que reemplazar la implementación de Func / Action en una prueba de Unidad es tan simple como simplemente asignar un nuevo valor a Func / Action
  4. También permite probar si se llamó a un Func estático desde otro método ya que los métodos estáticos no se pueden burlar.
  5. Fácil de refactorizar los métodos existentes para Funcs / Actions ya que la sintaxis para llamar a un método en el sitio de la llamada sigue siendo la misma. (Para los momentos en que no puede refactorizar un método en una Func / Action, vea los contras)

Contras

  1. Las funciones / acciones no se pueden usar si su clase se puede derivar ya que las funciones / acciones no tienen rutas de herencia como los métodos
  2. No se pueden usar los parámetros predeterminados. Crear un Func con parámetros predeterminados implica crear un nuevo delegado que puede hacer que el código sea confuso dependiendo del caso de uso
  3. No se puede usar la sintaxis de parámetro con nombre para llamar a métodos como algo (firstName: "S", lastName: "K")
  4. La mayor desventaja es que no tiene acceso a la referencia 'this' en Funcs and Actions, por lo que cualquier dependencia de la clase debe pasarse explícitamente como parámetros. Es bueno, ya que conoce todas las dependencias, pero malo si tiene muchas propiedades de las que dependerá su Func. Su millaje variará según su caso de uso.

En resumen, usar Funcs and Actions for Unit test es excelente si sabes que tus clases nunca serán anuladas.

Además, generalmente no creo propiedades para Funcs, sino que las alineo directamente como tal

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

¡Espero que esto ayude!

Salman Hasrat Khan
fuente
-1

Usando Mock es posible. Nuget: https://www.nuget.org/packages/moq/

Y créeme, es bastante simple y tiene sentido.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock necesita un método virtual para anular.

BrightFuture1029
fuente