with unit test services run component unit-testing dependency-injection dependencies

unit-testing - services - unit test directive angular 2



Código de prueba unitaria con una dependencia del sistema de archivos (11)

¡Hurra! Ahora es comprobable; Puedo alimentar a los dobles de prueba (simulacros) con el método DoIt. ¿Pero a qué precio? Ahora he tenido que definir 3 nuevas interfaces solo para que esto sea comprobable. ¿Y qué, exactamente, estoy probando? Estoy probando que mi función DoIt interactúa correctamente con sus dependencias. No prueba que el archivo comprimido se haya descomprimido correctamente, etc.

Has dado en el clavo justo en su cabeza. Lo que quiere probar es la lógica de su método, no necesariamente si se puede abordar un archivo verdadero. No es necesario que pruebe (en esta prueba unitaria) si un archivo está descomprimido correctamente, su método lo da por sentado. Las interfaces son valiosas por sí mismas porque proporcionan abstracciones contra las que puede programar, en lugar de depender implícita o explícitamente de una implementación concreta.

Estoy escribiendo un componente que, dado un archivo ZIP, necesita:

  1. Descomprime el archivo.
  2. Encuentre un dll específico entre los archivos descomprimidos.
  3. Cargue ese dll mediante reflexión e invoque un método en él.

Me gustaría probar la unidad de este componente.

Estoy tentado de 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, la base de datos, la red, etc."

Si tuviera que escribir esto de una manera amigable con la prueba de unidad, 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 a los dobles de prueba (simulacros) con el método DoIt. ¿Pero a qué precio? Ahora he tenido que definir 3 nuevas interfaces solo para que esto sea comprobable. ¿Y qué, exactamente, estoy probando? Estoy probando que mi función DoIt interactúa correctamente con sus dependencias. No prueba que el archivo comprimido se haya descomprimido correctamente, etc.

Ya no siento que estoy probando la funcionalidad. Parece que estoy probando interacciones de clase.

Mi pregunta es esta : ¿cuál es la forma correcta de probar algo que depende del sistema de archivos?

edit Estoy usando .NET, pero el concepto también podría aplicar Java o código nativo.


Asumiendo que las "interacciones del sistema de archivos" están bien probadas en el marco mismo, crea tu método para trabajar con flujos y prueba esto. Abrir un FileStream y pasarlo al método puede quedar fuera de sus pruebas, ya que los creadores del framework prueban bien FileStream.Open.


Como otros han dicho, el primero está bien como una prueba de integración. El segundo prueba solo lo que se supone que la función realmente hace, que es todo lo que una prueba de unidad debería hacer.

Como se muestra, el segundo ejemplo parece un poco sin sentido, pero le da la oportunidad de probar cómo la función responde a los errores en cualquiera de los pasos. No tiene ninguna comprobación de errores en el ejemplo, pero sí en el sistema real, y la inyección de dependencia le permitirá probar todas las respuestas a cualquier error. Entonces el costo habrá valido la pena.


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.

Extraería el código que trata del 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 de DllLoader usando la interfaz y compruebe que ... usted no es responsable del código de descompresión, ¿verdad?


No debe probar la interacción de clase y la función de llamada. en su lugar, debe considerar las pruebas de integración. Pruebe el resultado requerido y no la operación de carga del archivo.


No tiene nada de malo golpear el sistema de archivos, simplemente considérelo una prueba de integración en lugar de una prueba unitaria. Cambiaría la ruta codificada con 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 tiempo en ejecutarse, sepárelas para que no se ejecuten con tanta frecuencia como las pruebas de unidad rápida.

Estoy de acuerdo, a veces creo que las pruebas basadas en la interacción pueden causar demasiado acoplamiento y, a menudo, no proporcionan el valor suficiente. Realmente desea probar la descompresión del archivo aquí, no solo verificar que está llamando a los métodos correctos.


Para la prueba unitaria, le sugiero que incluya el archivo de prueba en su proyecto (archivo EAR o equivalente) y luego use una ruta relativa en las pruebas de la unidad, es decir "../testdata/testfile".

Siempre que su proyecto se exporte / importe correctamente de lo que debería funcionar su prueba unitaria.


Realmente no hay nada de malo en esto, solo se trata de si lo llamamos una prueba unitaria o una prueba de integración. Solo tienes que asegurarte de que si interactúas con el sistema de archivos, no hay 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 tenía el mismo nombre de archivo que el que estaba usando. Siempre use rutas relativas y no rutas absolutas.

También sería una buena idea incluir chdir() en un directorio temporal antes de ejecutar la prueba, y luego chdir() .


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 que genial, pero creo que a menudo no es el caso.

Mi opinión sobre esto es que las pruebas de su unidad harán tanto como puedan, que pueden no tener una cobertura del 100%. De hecho, solo puede ser 10%. El punto es que las pruebas de su unidad deben ser rápidas y no tener dependencias externas. Podrían 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 unidades) que pueden tener dependencias externas y probar escenarios de extremo a extremo como estos.

Al medir la cobertura del código, mido tanto la unidad como las pruebas de integración.


Su pregunta expone una de las partes más difíciles de las pruebas para los desarrolladores:

"¿Qué diablos pruebo?"

Su ejemplo no es muy interesante porque simplemente combina algunas llamadas de la API juntas, por lo que si escribiera una prueba unitaria, terminaría afirmando que se llamaron los métodos. Pruebas como esta combinan 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 al cambiar los detalles de implementación se rompen las pruebas!

Tener malas pruebas es en realidad peor que no tener ninguna prueba en absoluto.

En tu ejemplo:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner) { string path = zipper.Unzip(theZipFile); IFakeFile file = fileSystem.Open(path); runner.Run(file); }

Si bien se puede pasar en simulacros, no hay lógica en el método para probar. Si tuviera que intentar una prueba unitaria para esto, podría verse más o menos así:

// Assuming that zipper, fileSystem, and runner are mocks void testDoIt() { // mock behavior of the mock objects when(zipper.Unzip(any(File.class)).thenReturn("some path"); when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class)); // run the test someObject.DoIt(zipper, fileSystem, runner); // verify things were called verify(zipper).Unzip(any(File.class)); verify(fileSystem).Open("some path")); verify(runner).Run(file); }

Enhorabuena, básicamente copió y pegó los detalles de implementación de su método DoIt() en una prueba. Feliz mantenimiento.

Cuando escribe pruebas, quiere probar el QUÉ y no el CÓMO . Ver Black Box Testing para más.

El WHAT es el nombre de su método (o al menos debería ser). Los 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), ¿romperá mi (s) prueba (s)?"

Si la respuesta es sí, está probando el CÓMO y no el QUÉ .

Para responder a su pregunta específica sobre el código de prueba con las dependencias del sistema de archivos, supongamos que tiene algo más interesante que hacer con un archivo y desea guardar el contenido codificado en Base64 de un byte[] en un archivo. Puedes usar streams para probar que tu código hace lo correcto sin tener que verificar cómo lo hace. Un ejemplo podría ser algo como esto (en Java):

interface StreamFactory { OutputStream outStream(); InputStream inStream(); } class Base64FileWriter { public void write(byte[] contents, StreamFactory streamFactory) { OutputStream outputStream = streamFactory.outStream(); outputStream.write(Base64.encodeBase64(contents)); } } @Test public void save_shouldBase64EncodeContents() { OutputStream outputStream = new ByteArrayOutputStream(); StreamFactory streamFactory = mock(StreamFactory.class); when(streamFactory.outStream()).thenReturn(outputStream); // Run the method under test Base64FileWriter fileWriter = new Base64FileWriter(); fileWriter.write("Man".getBytes(), streamFactory); // Assert we saved the base64 encoded contents assertThat(outputStream.toString()).isEqualTo("TWFu"); }

La prueba usa un ByteArrayOutputStream pero en la aplicación (usando inyección de dependencia) la verdadera StreamFactory (quizás llamada FileStreamFactory) devolvería FileOutputStream desde outputStream() y escribiría en un File .

Lo que fue interesante sobre el método de write aquí es que estaba escribiendo el contenido codificado en Base64, así que eso es lo que probamos. Para su método DoIt() , esto sería más adecuadamente probado con una prueba de integración .


Una forma sería escribir el método de descompresión para tomar InputStreams. Entonces, la prueba unitaria podría construir un InputStream 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.