Pregunta

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

  1. Descomprima el archivo.
  2. Encuentra un dll específico entre los archivos descomprimidos.
  3. Cargue ese dll a través de la reflexión e invoque un método sobre él.

Me gustaría probar este componente por unidad.

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 se basen en el sistema de archivos, la base de datos, la 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);
}

¡Yay! 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 esta : ¿cuál es la forma correcta de probar unitariamente algo que depende del sistema de archivos?

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

¿Fue útil?

Solución

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, 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 () en un directorio temporal antes de ejecutar su prueba, y chdir () nuevamente después.

Otros consejos

  

¡Yay! 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.

Has golpeado el clavo justo en su 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.

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, 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 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 rompe sus pruebas!

Tener malas pruebas es realmente peor que no tener ninguna prueba.

En su ejemplo:

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

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í:

// 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);
}

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

Cuando escribe pruebas, desea probar el QUÉ y no el CÓMO . Consulte Black Box Testing para obtener más información.

El QUÉ es el nombre de su método (o al menos debería serlo). 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énsalo de esta manera, pregúntate:

" 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 tenía algo más interesante que hacer con un archivo y desea guardar el contenido codificado en Base64 de un byte [] a 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):

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) el StreamFactory real (quizás llamado FileStreamFactory) devolvería FileOutputStream de outputStream () y escribiría en un File .

Lo interesante del método write aquí es que estaba escribiendo los contenidos codificados en Base64, así que eso fue lo que probamos. Para su método DoIt () , esto se probaría más adecuadamente con una prueba de integración .

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 las 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 utilizando 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.

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 sus pruebas de integración tardan demasiado en ejecutarse, sepárelas para que no se ejecuten con tanta frecuencia como sus pruebas de unidad 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.

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.

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 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?

Suponiendo que " interacciones del sistema de archivos " están bien probados en el propio marco, crea tu método para trabajar con secuencias y pruébalo. 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 marco.

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.

Para pruebas unitarias, 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".

Mientras su proyecto se exporte / importe correctamente, su prueba de unidad debería funcionar.

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top