¿Cómo puedo unir los métodos de prueba que usan métodos estáticos?

8

Supongamos que escribí un método de extensión en C # para bytematrices que los codifica en cadenas hexadecimales, de la siguiente manera:

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

Podría probar el método anterior usando NUnit de la siguiente manera:

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Si uso el Extensions.ToHexinterior de mi proyecto, supongamos en el Foo.Dométodo de la siguiente manera:

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

Entonces, todas las pruebas Foo.Dodependerán del éxito de TestToHex_Works.

Usando funciones libres en C ++, el resultado será el mismo: las pruebas que prueban métodos que usan funciones libres dependerán del éxito de las pruebas de función libre.

¿Cómo puedo manejar tales situaciones? ¿Puedo resolver de alguna manera estas dependencias de prueba? ¿Hay una mejor manera de probar los fragmentos de código anteriores?

Akira
fuente
44
Then all tests of Foo.Do will depend on the success of TestToHex_works-- ¿Entonces? ¿No tienes clases que dependen del éxito de otras clases?
Robert Harvey
55
Nunca he entendido esta obsesión con las funciones libres / estáticas y su llamada no verificabilidad. Si una función libre está libre de efectos secundarios, es lo más fácil del planeta probar y demostrar que funciona. Lo has demostrado con bastante eficacia en tu propia pregunta. ¿Cómo se prueban los métodos ordinarios libres de efectos secundarios (que no dependen del estado de la clase) en instancias de objetos? Sé que tienes algunos de esos.
Robert Harvey
2
El único inconveniente de este código que usa funciones estáticas es que no puede usar fácilmente otra cosa que toHex(o intercambiar implementaciones). Aparte de eso, todo está bien. Su código de conversión a hexadecimal se prueba, ahora hay otro código que usa ese código probado como una utilidad para lograr su propio objetivo.
Steve Chamaillard
3
Me estoy perdiendo por completo cuál es el problema aquí. Si ToHex no funciona, entonces está claro que Do tampoco funcionará.
Simon B
55
La prueba para Foo.Do () no debería saber o importarle que llame a ToHex () debajo de las cubiertas, eso es un detalle de implementación.
17 de 26

Respuestas:

37

Entonces, todas las pruebas Foo.Dodependerán del éxito de TestToHex_Works.

Si. Es por eso que tienes pruebas para TextToHex. Si esas pruebas pasan, la función cumple con la especificación definida en esas pruebas. Por Foo.Dolo tanto, puede llamarlo con seguridad y no preocuparse por eso. Ya está cubierto.

Puede agregar una interfaz, convertir el método en un método de instancia e inyectarlo Foo. Entonces podrías burlarte TextToHex. Pero ahora tienes que escribir un simulacro, que puede funcionar de manera diferente. Por lo tanto, necesitará una prueba de "integración" para unir los dos y garantizar que las partes realmente funcionen juntas. ¿Qué ha logrado eso además de hacer las cosas más complejas?

La idea de que las pruebas unitarias deberían probar partes de su código aisladas de otras partes es una falacia. La "unidad" en una prueba de unidad es una unidad de ejecución aislada. Si dos pruebas pueden ejecutarse simultáneamente sin afectarse entre sí, entonces se ejecutan de forma aislada y también lo son las pruebas unitarias. Las funciones estáticas que son rápidas, no tienen una configuración compleja y no tienen efectos secundarios como su ejemplo, por lo tanto, están bien para usar directamente en pruebas unitarias. Si tiene un código lento, complejo de configurar o tiene efectos secundarios, entonces los simulacros son útiles. Sin embargo, deben evitarse en otros lugares.

David Arno
fuente
2
Usted hace un argumento bastante bueno para el desarrollo de "prueba primero". La burla existe en parte porque el código se escribe de tal manera que es demasiado difícil de probar, y si escribe sus pruebas primero, se obliga a escribir código que sea más fácilmente comprobable.
Robert Harvey
3
Estoy votando puramente por recomendar no burlarse de las funciones puras. Visto eso demasiadas veces.
Jared Smith
2

Usando funciones libres en C ++, el resultado será el mismo: las pruebas que prueban métodos que usan funciones libres dependerán del éxito de las pruebas de función libre.

¿Cómo puedo manejar tales situaciones? ¿Puedo resolver de alguna manera estas dependencias de prueba? ¿Hay una mejor manera de probar los fragmentos de código anteriores?

Bueno, no veo dependencia aquí. Al menos no del tipo que nos obliga a ejecutar una prueba antes que otra. La dependencia que construimos entre las pruebas (sin importar el tipo) es una confianza .

Construimos un código (prueba primero o no) y nos aseguramos de que las pruebas pasen. Entonces, estamos en posición de construir más código. Todo el código construido sobre esto primero se basa en la confianza y la certeza. Esto es más o menos lo que @DavidArno explica (muy bien) en su respuesta.

Si. Es por eso que tiene pruebas para X. Si esas pruebas pasan, la función cumple con la especificación definida en esas pruebas. Entonces Y puede llamarlo con seguridad y no preocuparse por eso. Ya está cubierto.

Las pruebas unitarias deben ejecutarse en cualquier orden, en cualquier momento, en cualquier entorno y lo más rápido posible. Si TestToHex_Worksse ejecuta el primero o el último no debería preocuparte.

Si TestToHex_Worksfalla debido a errores ToHex, todas las pruebas en las que confíe ToHexterminarán con resultados diferentes y fallarán (idealmente). La clave aquí es detectar esos resultados diferentes. Lo hacemos haciendo pruebas unitarias para ser deterministas. Como haces aqui

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

Otras pruebas unitarias que dependan ToHextambién deben ser deterministas. Si todo ToHexva bien, el resultado debería ser el esperado. Si obtiene uno diferente, algo salió mal, en algún lugar y esto es lo que desea de una prueba unitaria, para detectar estos cambios sutiles y fallar rápidamente.

Laiv
fuente
1

Es un poco complicado con un ejemplo tan mínimo, pero reconsideremos lo que estamos haciendo:

Supongamos que escribí un método de extensión en c # para conjuntos de bytes que los codifica en cadenas hexadecimales, de la siguiente manera:

¿Por qué escribiste un método de extensión para hacer esto? A menos que esté escribiendo una biblioteca para codificar matrices de bytes en cadenas, es probable que este no sea el requisito que está tratando de cumplir. Lo que parece que estabas haciendo aquí era tratar de cumplir el requisito "Validar una carga útil que es una matriz de bytes", por lo que las pruebas unitarias que estás implementando deberían ser "Dada una carga útil válida X, mi método devuelve verdadero" y "Dado una carga útil no válida, mi método devuelve falso ".

Entonces, cuando implementa esto inicialmente, puede hacer las cosas "byte-to-hex" en línea en el método. Y eso está bien. Luego, más tarde, tiene algún otro requisito (por ejemplo, "Mostrar la carga útil como una cadena hexadecimal") que, mientras lo implementa, se da cuenta de que también requiere que convierta una matriz de bytes en una cadena hexadecimal. En ese momento, crea su método de extensión, refactoriza el método anterior y lo llama desde su nuevo código. Sus pruebas para la funcionalidad de validación no deberían cambiar. Sus pruebas para mostrar la carga útil deben ser las mismas si el código de bytes a hexadecimal estaba en línea o en un método estático. El método estático es un detalle de implementación que no debería importarle al escribir las pruebas. El código en el método estático será probado por sus consumidores. Yo no

Chris Cooper
fuente
"No es probable que este sea el requisito que estás tratando de cumplir. Lo que parece que estabas haciendo aquí fue tratar de cumplir el requisito" - estás haciendo muchas suposiciones sobre el propósito de una función que probablemente fue elegido como un ejemplo representativo, y no algo de un programa más amplio.
cuál es el
La "unidad" en "pruebas unitarias" no significa una sola función o clase. Significa una unidad de funcionalidad. A menos que esté escribiendo específicamente una biblioteca que convierta bytes a cadenas, esa parte es en gran medida un detalle de implementación de un poco más de comportamiento útil. Ese comportamiento útil es la parte para la que desea hacerse las pruebas, porque esa es la parte que le interesa para ser correcto. En un mundo ideal, desea poder encontrar una biblioteca que ya existe para hacer el bin-to-hex (o lo que sea, no se obsesione con los detalles), cámbiela por su implementación y no cambie ninguna pruebas
Chris Cooper
El diablo está en los detalles. Los "detalles de implementación" de las pruebas unitarias siguen siendo una tarea enormemente útil y no es algo que se pueda pasar por alto.
whatsisname
0

Exactamente. Y este es uno de los problemas con los métodos estáticos, otro es que OOP es una alternativa mucho mejor en la mayoría de las situaciones.

Esta es también una de las razones por las que se usa la inyección de dependencia.

En su caso, puede preferir tener un convertidor concreto que inyecte en una clase que necesita una conversión de algunos valores a hexadecimal. Una interfaz , implementada por esta clase concreta, definiría el contrato requerido para convertir los valores.

No solo podrá probar su código más fácilmente, sino que también podrá intercambiar implementaciones más tarde (ya que hay muchas otras formas posibles de convertir valores a hexadecimales, algunas de las cuales producen diferentes salidas).

Arseni Mourzenko
fuente
2
¿Cómo eso no hará que el código dependa del éxito de las pruebas del convertidor? Inyectar una clase, una interfaz o cualquier cosa que desee no hace que funcione mágicamente. Si estaba hablando de los efectos secundarios, entonces sí, habría un montón de valor inyectando una falsificación en lugar de codificar una implementación, pero ese problema nunca existió en primer lugar de todos modos.
Steve Chamaillard
1
@ArseniMourzenko, así que al inyectar una clase de calculadora que implementa un multiplymétodo, ¿te burlarías de esto? ¿Qué significa para refactorizar (y usar el operador * en su lugar), tendrá que cambiar cada declaración simulada en su código? No me parece un código fácil. Todo lo que digo es que toHexes muy simple y no tiene ningún efecto secundario, así que no veo ninguna razón para burlarse de esto. Es demasiado costo para absolutamente ninguna ganancia. Sin embargo, no digo que no debamos inyectarlo, pero eso depende del uso.
Steve Chamaillard
2
@Ewan, si modifico un código y luego me enfrento a " un mar de rojo ", podría pensar oh no, ¿por dónde empiezo? Pero tal vez podría volver al código que acabo de modificar y mirar primero sus pruebas para ver qué rompí. : p
David Arno
3
@ArseniMourzenko, si DI mejora mucho su diseño, ¿por qué no veo argumentos para inyectar tipos como Stringy inttambién? ¿O clases básicas de utilidad de la biblioteca estándar?
Bart van Ingen Schenau
44
Su respuesta ignora por completo los aspectos prácticos. Hay muchas situaciones con métodos estáticos que son rápidos, autónomos y lo suficientemente improbables como para cambiar, y que el esfuerzo de inyectarlos o saltar a través de cualquier aro que no sea una llamada de función normal, es una pérdida de tiempo inútil. esfuerzo.
cuál es el