Cómo hacer pruebas unitarias de funciones escribiendo archivos usando python unittest

81

Tengo una función de Python que escribe un archivo de salida en el disco.

Quiero escribir una prueba unitaria para ello usando el módulo de prueba unitaria de Python.

¿Cómo debo afirmar la igualdad de archivos? Me gustaría recibir un error si el contenido del archivo difiere del esperado + lista de diferencias. Como en la salida del comando Unix diff.

¿Existe alguna forma oficial / recomendada de hacerlo?

ene
fuente

Respuestas:

48

Lo más simple es escribir el archivo de salida, luego leer su contenido, leer el contenido del archivo dorado (esperado) y compararlo con una simple igualdad de cadenas. Si son iguales, elimine el archivo de salida. Si son diferentes, haga una afirmación.

De esta manera, cuando se realizan las pruebas, cada prueba fallida se representará con un archivo de salida, y puede usar una herramienta de terceros para compararlas con los archivos dorados (Beyond Compare es maravilloso para esto).

Si realmente desea proporcionar su propia salida de diferencias, recuerde que Python stdlib tiene el módulo difflib. El nuevo soporte de unittest en Python 3.1 incluye un assertMultiLineEqualmétodo que lo usa para mostrar diferencias, similar a esto:

    def assertMultiLineEqual(self, first, second, msg=None):
        """Assert that two multi-line strings are equal.

        If they aren't, show a nice diff.

        """
        self.assertTrue(isinstance(first, str),
                'First argument is not a string')
        self.assertTrue(isinstance(second, str),
                'Second argument is not a string')

        if first != second:
            message = ''.join(difflib.ndiff(first.splitlines(True),
                                                second.splitlines(True)))
            if msg:
                message += " : " + msg
            self.fail("Multi-line strings are unequal:\n" + message)
Ned Batchelder
fuente
70

Prefiero que las funciones de salida acepten explícitamente un identificador de archivo (u objeto similar a un archivo), en lugar de aceptar un nombre de archivo y abrir el archivo ellos mismos. De esta manera, puedo pasar un StringIOobjeto a la función de salida en mi prueba unitaria, luego .read()el contenido de ese StringIOobjeto (después de una .seek(0)llamada) y compararlo con mi salida esperada.

Por ejemplo, haríamos una transición de código como este

##File:lamb.py
import sys


def write_lamb(outfile_path):
    with open(outfile_path, 'w') as outfile:
        outfile.write("Mary had a little lamb.\n")


if __name__ == '__main__':
    write_lamb(sys.argv[1])



##File test_lamb.py
import unittest
import tempfile

import lamb


class LambTests(unittest.TestCase):
    def test_lamb_output(self):
        outfile_path = tempfile.mkstemp()[1]
        try:
            lamb.write_lamb(outfile_path)
            contents = open(tempfile_path).read()
        finally:
            # NOTE: To retain the tempfile if the test fails, remove
            # the try-finally clauses
            os.remove(outfile_path)
        self.assertEqual(result, "Mary had a little lamb.\n")

codificar como este

##File:lamb.py
import sys


def write_lamb(outfile):
    outfile.write("Mary had a little lamb.\n")


if __name__ == '__main__':
    with open(sys.argv[1], 'w') as outfile:
        write_lamb(outfile)



##File test_lamb.py
import unittest
from io import StringIO

import lamb


class LambTests(unittest.TestCase):
    def test_lamb_output(self):
        outfile = StringIO()
        # NOTE: Alternatively, for Python 2.6+, you can use
        # tempfile.SpooledTemporaryFile, e.g.,
        #outfile = tempfile.SpooledTemporaryFile(10 ** 9)
        lamb.write_lamb(outfile)
        outfile.seek(0)
        content = outfile.read()
        self.assertEqual(content, "Mary had a little lamb.\n")

Este enfoque tiene el beneficio adicional de hacer que su función de salida sea más flexible si, por ejemplo, decide que no desea escribir en un archivo, sino en algún otro búfer, ya que aceptará todos los objetos similares a archivos.

Tenga en cuenta que el uso StringIOsupone que el contenido de la salida de prueba puede caber en la memoria principal. Para una salida muy grande, puede utilizar un enfoque de archivo temporal (por ejemplo, tempfile.SpooledTemporaryFile ).

gotgenes
fuente
2
Esto es mejor que escribir un archivo en el disco. Si está ejecutando toneladas de pruebas unitarias, IO a disco causa todo tipo de problemas, especialmente al tratar de limpiarlos. Tuve pruebas de escritura en disco, el tearDown borrando los archivos escritos. Las pruebas funcionarían bien una a la vez, luego fallarían cuando se ejecutaran todas. Al menos con Visual Studio y PyTools en una máquina Win. Además, velocidad.
srock
1
Si bien esta es una buena solución para probar funciones separadas, sigue siendo problemático al probar la interfaz real que proporciona su programa (por ejemplo, una herramienta CLI) ..
Joost
1
Recibí un error: TypeError: se esperaba un argumento unicode, obtuve 'str'
cn123h
Vine aquí porque estoy tratando de escribir pruebas unitarias para caminar y leer conjuntos de datos de parquet particionados archivo por archivo. Esto requiere analizar la ruta del archivo para obtener los pares clave / valor para asignar el valor apropiado de una partición a (en última instancia) el DataFrame de pandas resultante. Escribir en un búfer, aunque es bueno, no me da la capacidad de analizar los valores de partición.
PMende
1
@PMende Parece que estás trabajando con una API que necesita interacción con un sistema de archivos real. Las pruebas unitarias no siempre son el nivel apropiado de prueba. Está bien no probar todas las partes de su código a nivel de pruebas unitarias; Las pruebas de integración o del sistema también deben usarse cuando sea apropiado. Sin embargo, trate de contener esas partes y pase solo valores simples entre los límites siempre que sea posible. Ver youtube.com/watch?v=eOYal8elnZk
gotgenes
20
import filecmp

Entonces

self.assertTrue(filecmp.cmp(path1, path2))
tbc0
fuente
2
De forma predeterminada, esto hace una shallowcomparación que verifica solo los metadatos de los archivos (mtime, size, etc.). Agregue shallow=Falsesu ejemplo.
famzah
2
Además, los resultados se almacenan en caché .
famzah
11

Siempre trato de evitar escribir archivos en el disco, incluso si es una carpeta temporal dedicada a mis pruebas: no tocar realmente el disco hace que tus pruebas sean mucho más rápidas, especialmente si interactúas mucho con archivos en tu código.

Suponga que tiene este software "asombroso" en un archivo llamado main.py:

"""
main.py
"""

def write_to_file(text):
    with open("output.txt", "w") as h:
        h.write(text)

if __name__ == "__main__":
    write_to_file("Every great dream begins with a dreamer.")

Para probar el write_to_filemétodo, puede escribir algo como esto en un archivo en la misma carpeta llamado test_main.py:

"""
test_main.py
"""
from unittest.mock import patch, mock_open

import main


def test_do_stuff_with_file():
    open_mock = mock_open()
    with patch("main.open", open_mock, create=True):
        main.write_to_file("test-data")

    open_mock.assert_called_with("output.txt", "w")
    open_mock.return_value.write.assert_called_once_with("test-data")
Enrico M.
fuente
3

Puede separar la generación de contenido del manejo de archivos. De esa manera, puede probar que el contenido es correcto sin tener que perder el tiempo con archivos temporales y limpiarlos después.

Si escribe un método generador que produce cada línea de contenido, entonces puede tener un método de manejo de archivos que abre un archivo y llama file.writelines()con la secuencia de líneas. Los dos métodos podrían incluso estar en la misma clase: el código de prueba llamaría al generador y el código de producción llamaría al controlador de archivos.

A continuación, se muestra un ejemplo que muestra las tres formas de realizar la prueba. Por lo general, solo elegiría uno, dependiendo de los métodos disponibles en la clase para probar.

import os
from io import StringIO
from unittest.case import TestCase


class Foo(object):
    def save_content(self, filename):
        with open(filename, 'w') as f:
            self.write_content(f)

    def write_content(self, f):
        f.writelines(self.generate_content())

    def generate_content(self):
        for i in range(3):
            yield u"line {}\n".format(i)


class FooTest(TestCase):
    def test_generate(self):
        expected_lines = ['line 0\n', 'line 1\n', 'line 2\n']
        foo = Foo()

        lines = list(foo.generate_content())

        self.assertEqual(expected_lines, lines)

    def test_write(self):
        expected_text = u"""\
line 0
line 1
line 2
"""
        f = StringIO()
        foo = Foo()

        foo.write_content(f)

        self.assertEqual(expected_text, f.getvalue())

    def test_save(self):
        expected_text = u"""\
line 0
line 1
line 2
"""
        foo = Foo()

        filename = 'foo_test.txt'
        try:
            foo.save_content(filename)

            with open(filename, 'rU') as f:
                text = f.read()
        finally:
            os.remove(filename)

        self.assertEqual(expected_text, text)
Don Kirkby
fuente
¿Podría proporcionar un código de ejemplo para eso? Suena interesante.
buhtz
1
Agregué un ejemplo para los tres enfoques, @buhtz.
Don Kirkby
-1

Basado en sugerencias, hice lo siguiente.

class MyTestCase(unittest.TestCase):
    def assertFilesEqual(self, first, second, msg=None):
        first_f = open(first)
        first_str = first_f.read()
        second_f = open(second)
        second_str = second_f.read()
        first_f.close()
        second_f.close()

        if first_str != second_str:
            first_lines = first_str.splitlines(True)
            second_lines = second_str.splitlines(True)
            delta = difflib.unified_diff(first_lines, second_lines, fromfile=first, tofile=second)
            message = ''.join(delta)

            if msg:
                message += " : " + msg

            self.fail("Multi-line strings are unequal:\n" + message)

Creé una subclase MyTestCase ya que tengo muchas funciones que necesitan leer / escribir archivos, por lo que realmente necesito tener un método de aserción reutilizable. Ahora, en mis pruebas, subclase MyTestCase en lugar de unittest.TestCase.

¿Qué piensa usted al respecto?

ene
fuente