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

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

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

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

¿Hay alguna forma oficial / recomendada de hacer eso?

Lo más simple es escribir el archivo de salida, luego leer su contenido, leer el contenido del archivo dorado (esperado) y compararlos con la igualdad de cadena simple. Si son iguales, borre el archivo de salida. Si son diferentes, levante una aserción.

De esta manera, cuando se realicen las pruebas, cada prueba fallida se representará con un archivo de salida, y puede usar una herramienta de terceros para diferenciarlas de los archivos de oro (Más allá de la comparación 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 método assertMultiLineEqual 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) 

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 objeto StringIO a la función de salida en mi prueba de unidad, luego .read() el contenido de ese objeto StringIO (después de una .seek(0) a .seek(0) ) y comparar con mi salida esperada.

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

 ##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, eg, #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 la ventaja 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 de StringIO supone que el contenido de la salida de prueba puede caber en la memoria principal. Para resultados muy grandes, puede utilizar un enfoque de archivo temporal (por ejemplo, tempfile.SpooledTemporaryFile ).

 import filecmp 

Entonces

 self.assertTrue(filecmp.cmp(path1, path2)) 

Podría separar la generación de contenido del manejo de archivos. De esa manera, puedes probar que el contenido es correcto sin tener que perder el tiempo con archivos temporales y luego limpiarlos.

Si escribe un método generador que produce cada línea de contenido, entonces puede tener un método de manejo de archivos que abra un archivo y llame a 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 manejador de archivos.

Aquí hay un ejemplo que muestra las tres formas de probar. Por lo general, solo elegiría uno, dependiendo de los métodos disponibles en la clase para evaluar.

 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) 

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

Supongamos que tiene este software “increíble” 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 método write_to_file , puedes escribir algo como esto en un archivo en la misma carpeta llamada 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") 

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, así que realmente necesito tener un método de afirmación reutilizable. Ahora en mis pruebas, yo subclasificaría MyTestCase en lugar de unittest.TestCase.

¿Qué piensa usted al respecto?