Estoy escribiendo un componente que, dado un archivo ZIP, necesita:
- Descomprime el archivo.
- Encuentra una dll específica entre los archivos descomprimidos.
- Cargue ese dll a través de la reflexión e invoque un método sobre él.
Me gustaría probar unitariamente este componente.
Estoy tentado a escribir código que trate directamente con el sistema de archivos:
void DoIt()
{
Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
myDll.InvokeSomeSpecialMethod();
}
Pero la gente suele decir: "No escriba pruebas unitarias que dependan del sistema de archivos, base de datos, red, etc."
Si tuviera que escribir esto de una manera amigable para las pruebas unitarias, supongo que se vería así:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
¡Hurra! Ahora es comprobable; Puedo alimentar en dobles de prueba (simulacros) al método DoIt. ¿Pero a qué precio? Ahora he tenido que definir 3 nuevas interfaces solo para hacer que esto sea comprobable. ¿Y qué estoy probando exactamente? Estoy probando que mi función DoIt interactúa correctamente con sus dependencias. No prueba que el archivo zip se haya descomprimido correctamente, etc.
Ya no parece que esté probando la funcionalidad. Parece que solo estoy probando las interacciones de clase.
Mi pregunta es la siguiente : ¿cuál es la forma correcta de probar la unidad algo que depende del sistema de archivos?
editar Estoy usando .NET, pero el concepto podría aplicar Java o código nativo también.
fuente
myDll.InvokeSomeSpecialMethod();
donde verificaría que funciona correctamente tanto en situaciones de éxito como de fracaso, por lo que no haría una prueba unitaria,DoIt
peroDllRunner.Run
dicho mal uso de una prueba UNIT para verificar que todo el proceso funcione un mal uso aceptable y, como sería una prueba de integración enmascarando una prueba unitaria, las reglas normales de la prueba unitaria no necesitan aplicarse estrictamenteRespuestas:
Realmente no hay nada de malo en esto, es solo una cuestión de si lo llaman prueba de unidad o prueba de integración. Solo tiene que asegurarse de que si interactúa con el sistema de archivos, no haya efectos secundarios no deseados. Específicamente, asegúrese de limpiar después de usted mismo, elimine los archivos temporales que haya creado y de que no sobrescriba accidentalmente un archivo existente que tenga el mismo nombre de archivo que un archivo temporal que estaba usando. Utilice siempre rutas relativas y no rutas absolutas.
También sería una buena idea
chdir()
ingresar a un directorio temporal antes de ejecutar su prueba, ychdir()
luego volver.fuente
chdir()
abarca todo el proceso, por lo que podría interrumpir la capacidad de ejecutar sus pruebas en paralelo, si su marco de prueba o una versión futura del mismo lo admite.Has golpeado el clavo justo en la cabeza. Lo que desea probar es la lógica de su método, no necesariamente si se puede abordar un archivo verdadero. No necesita probar (en esta prueba unitaria) si un archivo está descomprimido correctamente, su método lo da por sentado. Las interfaces son valiosas en sí mismas porque proporcionan abstracciones contra las que puede programar, en lugar de depender implícita o explícitamente de una implementación concreta.
fuente
DoIt
función comprobable como se indica ni siquiera necesita pruebas. Como señaló correctamente el interrogador, no queda nada de importancia para probar. Ahora es la implementación deIZipper
,IFileSystem
yIDllRunner
eso necesita pruebas, ¡pero son las cosas que se han burlado para la prueba!Su pregunta expone una de las partes más difíciles de las pruebas para los desarrolladores que solo se involucran:
"¿Qué demonios pruebo?"
Su ejemplo no es muy interesante porque simplemente pega algunas llamadas de API juntas, por lo que si escribiera una prueba unitaria para ello, terminaría simplemente afirmando que se llamaron a los métodos. Pruebas como esta combinan estrechamente los detalles de su implementación con la prueba. ¡Esto es malo porque ahora tiene que cambiar la prueba cada vez que cambia los detalles de implementación de su método porque cambiar los detalles de implementación interrumpe su (s) prueba (s)!
Tener malas pruebas es en realidad peor que no tener ninguna prueba.
En tu ejemplo:
Si bien puede pasar simulacros, no hay lógica en el método para probar. Si intentara una prueba unitaria para esto, podría verse así:
Felicitaciones, básicamente copió y pegó los detalles de implementación de su
DoIt()
método en una prueba. Feliz mantenimientoCuando escribes pruebas quieres probar el QUÉ y no el CÓMO . Ver Prueba de caja negra para más.
El WHAT es el nombre de su método (o al menos debería serlo). El CÓMO son todos los pequeños detalles de implementación que viven dentro de su método. Las buenas pruebas le permiten cambiar el CÓMO sin romper el QUÉ .
Piénselo de esta manera, pregúntese:
"Si cambio los detalles de implementación de este método (sin alterar el contrato público) ¿se romperán mis pruebas?"
Si la respuesta es sí, está probando el CÓMO y no el QUÉ .
Para responder a su pregunta específica sobre la prueba de código con dependencias del sistema de archivos, supongamos que tiene algo más interesante que hacer con un archivo y desea guardar el contenido codificado de Base64 de
byte[]
a en un archivo. Puede usar transmisiones para esto para probar que su código hace lo correcto sin tener que verificar cómo lo hace. Un ejemplo podría ser algo como esto (en Java):La prueba utiliza un
ByteArrayOutputStream
sino en la aplicación (mediante la inyección de dependencias) del StreamFactory real (tal vez llamado FileStreamFactory) devolveríaFileOutputStream
desdeoutputStream()
y escribiría a unaFile
.Lo interesante del
write
método aquí es que estaba escribiendo los contenidos codificados en Base64, así que eso es lo que probamos. Para suDoIt()
método, esto se probaría más adecuadamente con una prueba de integración .fuente
Soy reticente a contaminar mi código con tipos y conceptos que existen solo para facilitar las pruebas unitarias. Claro, si hace que el diseño sea más limpio y mejor, entonces genial, pero creo que a menudo ese no es el caso.
Mi opinión sobre esto es que sus pruebas unitarias harían todo lo posible, lo que puede no ser una cobertura del 100%. De hecho, solo puede ser del 10%. El punto es que las pruebas unitarias deben ser rápidas y no tener dependencias externas. Pueden probar casos como "este método arroja una excepción ArgumentNullException cuando pasa nulo para este parámetro".
Luego agregaría pruebas de integración (también automatizadas y probablemente usando el mismo marco de prueba de unidad) que pueden tener dependencias externas y probar escenarios de extremo a extremo como estos.
Al medir la cobertura del código, mido las pruebas unitarias y de integración.
fuente
No hay nada de malo en golpear el sistema de archivos, solo considérelo una prueba de integración en lugar de una prueba unitaria. Cambiaría la ruta codificada por una ruta relativa y crearía una subcarpeta TestData para contener las cremalleras para las pruebas unitarias.
Si las pruebas de integración tardan demasiado en ejecutarse, sepárelas para que no se ejecuten con tanta frecuencia como las pruebas de unidades rápidas.
Estoy de acuerdo, a veces creo que las pruebas basadas en la interacción pueden causar demasiado acoplamiento y, a menudo, no proporcionan suficiente valor. Realmente desea probar descomprimir el archivo aquí, no solo verificar que está llamando a los métodos correctos.
fuente
Una forma sería escribir el método de descompresión para tomar InputStreams. Luego, la prueba unitaria podría construir tal InputStream a partir de una matriz de bytes usando ByteArrayInputStream. El contenido de esa matriz de bytes podría ser una constante en el código de prueba de la unidad.
fuente
Esto parece ser más una prueba de integración, ya que depende de un detalle específico (el sistema de archivos) que podría cambiar, en teoría.
Resumiría el código que trata con el sistema operativo en su propio módulo (clase, ensamblaje, jar, lo que sea). En su caso, desea cargar una DLL específica si la encuentra, por lo tanto, cree una interfaz IDllLoader y una clase DllLoader. Haga que su aplicación adquiera la DLL del DllLoader usando la interfaz y pruebe que ... no es responsable del código de descompresión después de todo, ¿verdad?
fuente
Suponiendo que las "interacciones del sistema de archivos" se prueban bien en el propio marco, cree su método para trabajar con secuencias y pruébelo. Abrir un FileStream y pasarlo al método puede quedar fuera de sus pruebas, ya que FileStream.Open está bien probado por los creadores del framework.
fuente
No debe probar la interacción de clase y la llamada a funciones. en su lugar, debería considerar las pruebas de integración. Pruebe el resultado requerido y no la operación de carga de archivos.
fuente
Para la prueba unitaria, sugeriría que incluya el archivo de prueba en su proyecto (archivo EAR o equivalente) y luego use una ruta relativa en las pruebas unitarias, es decir, "../testdata/testfile".
Siempre y cuando su proyecto se exporte / importe correctamente, su prueba unitaria debería funcionar.
fuente
Como han dicho otros, el primero está bien como prueba de integración. La segunda prueba solo lo que se supone que debe hacer la función, que es todo lo que debe hacer una prueba unitaria.
Como se muestra, el segundo ejemplo parece un poco inútil, pero le da la oportunidad de probar cómo responde la función a los errores en cualquiera de los pasos. No tiene ninguna comprobación de errores en el ejemplo, pero en el sistema real que puede tener, y la inyección de dependencia le permitiría probar todas las respuestas a cualquier error. Entonces el costo habrá valido la pena.
fuente