Supongamos que escribí un método de extensión en C # para byte
matrices 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.ToHex
interior de mi proyecto, supongamos en el Foo.Do
mé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.Do
dependerá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?
unit-testing
static-methods
Akira
fuente
fuente
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?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.Respuestas:
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. PorFoo.Do
lo 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 burlarteTextToHex
. 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.
fuente
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.
Las pruebas unitarias deben ejecutarse en cualquier orden, en cualquier momento, en cualquier entorno y lo más rápido posible. Si
TestToHex_Works
se ejecuta el primero o el último no debería preocuparte.Si
TestToHex_Works
falla debido a erroresToHex
, todas las pruebas en las que confíeToHex
terminará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 aquiOtras pruebas unitarias que dependan
ToHex
también deben ser deterministas. Si todoToHex
va 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.fuente
Es un poco complicado con un ejemplo tan mínimo, pero reconsideremos lo que estamos haciendo:
¿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
fuente
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).
fuente
multiply
mé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 quetoHex
es 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.String
yint
también? ¿O clases básicas de utilidad de la biblioteca estándar?