So führen Sie Unit-Tests von Funktionen durch, die Dateien mit Python Unittest schreiben

81

Ich habe eine Python-Funktion, die eine Ausgabedatei auf die Festplatte schreibt.

Ich möchte einen Unit-Test dafür mit dem Python-Unittest-Modul schreiben.

Wie soll ich die Gleichheit der Dateien behaupten? Ich möchte eine Fehlermeldung erhalten, wenn der Dateiinhalt von der erwarteten + Liste der Unterschiede abweicht. Wie bei der Ausgabe des Unix Diff-Befehls.

Gibt es eine offizielle / empfohlene Möglichkeit, dies zu tun?

jan
quelle

Antworten:

48

Am einfachsten ist es, die Ausgabedatei zu schreiben, dann ihren Inhalt zu lesen, den Inhalt der (erwarteten) Golddatei zu lesen und sie mit der einfachen Zeichenfolgengleichheit zu vergleichen. Wenn sie identisch sind, löschen Sie die Ausgabedatei. Wenn sie unterschiedlich sind, erheben Sie eine Behauptung.

Auf diese Weise wird nach Abschluss der Tests jeder fehlgeschlagene Test mit einer Ausgabedatei dargestellt, und Sie können ein Drittanbieter-Tool verwenden, um sie von den Golddateien zu unterscheiden (Beyond Compare ist hierfür hervorragend geeignet).

Wenn Sie wirklich Ihre eigene Diff-Ausgabe bereitstellen möchten, denken Sie daran, dass die Python-Standardlib das Difflib-Modul enthält. Die neue unittest-Unterstützung in Python 3.1 enthält eine assertMultiLineEqualMethode, mit der Unterschiede angezeigt werden, ähnlich wie folgt:

    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
quelle
70

Ich ziehe Ausgabefunktionen haben explizit eine Datei akzeptieren Griff (oder dateiähnliche Objekt ), anstatt eine Datei übernehmen Namen und Öffnen der Datei selbst. Auf diese Weise kann ich StringIOin meinem Komponententest ein Objekt an die Ausgabefunktion übergeben, dann .read()den Inhalt von diesem StringIOObjekt (nach einem .seek(0)Aufruf) zurückgeben und mit meiner erwarteten Ausgabe vergleichen.

Zum Beispiel würden wir Code wie diesen übergehen

##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")

so zu codieren

##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")

Dieser Ansatz hat den zusätzlichen Vorteil, dass Ihre Ausgabefunktion flexibler wird, wenn Sie beispielsweise nicht in eine Datei schreiben möchten, sondern in einen anderen Puffer, da alle dateiähnlichen Objekte akzeptiert werden.

Beachten Sie, dass bei Verwendung StringIOdavon ausgegangen wird, dass der Inhalt der Testausgabe in den Hauptspeicher passt. Für sehr große Ausgaben können Sie einen temporären Dateiansatz verwenden (z . B. tempfile.SpooledTemporaryFile ).

gotgenes
quelle
2
Dies ist besser als das Schreiben einer Datei auf die Festplatte. Wenn Sie Tonnen von Unittests ausführen, verursacht E / A auf der Festplatte alle Arten von Problemen, insbesondere beim Versuch, sie zu bereinigen. Ich hatte Tests auf die Festplatte schreiben, die TearDown löschte die geschriebenen Dateien. Tests funktionieren nacheinander einwandfrei und schlagen dann fehl, wenn Alle ausgeführt werden. Zumindest mit Visual Studio und PyTools auf einem Win-Computer. Auch Geschwindigkeit.
Srock
1
Dies ist zwar eine gute Lösung, um separate Funktionen zu testen, aber es ist immer noch problematisch, wenn Sie die tatsächliche Schnittstelle testen, die Ihr Programm bereitstellt (z. B. ein CLI-Tool).
Joost
1
Ich habe Fehler erhalten: TypeError: Unicode-Argument erwartet, habe 'str'
cn123h
Ich bin hierher gekommen, weil ich versuche, Unit-Tests zum Gehen und Lesen von partitionierten Parkett-Datensätzen Datei für Datei zu schreiben. Dies erfordert das Analysieren des Dateipfads, um die Schlüssel / Wert-Paare zu erhalten, um den entsprechenden Wert einer Partition (letztendlich) dem resultierenden Pandas DataFrame zuzuweisen. Das Schreiben in einen Puffer ist zwar nett, bietet mir jedoch nicht die Möglichkeit, Partitionswerte zu analysieren.
PMende
1
@PMende Es hört sich so an, als würden Sie mit einer API arbeiten, die eine Interaktion mit einem tatsächlichen Dateisystem benötigt. Unit-Tests sind nicht immer das geeignete Testniveau. Es ist in Ordnung, nicht alle Teile Ihres Codes auf der Ebene von Komponententests zu testen. Gegebenenfalls sollten auch Integrations- oder Systemtests verwendet werden. Versuchen Sie jedoch, diese Teile zu enthalten, und übergeben Sie nach Möglichkeit nur einfache Werte zwischen den Grenzen. Siehe youtube.com/watch?v=eOYal8elnZk
gotgenes
20
import filecmp

Dann

self.assertTrue(filecmp.cmp(path1, path2))
tbc0
quelle
2
Mit dem Standard tut dies einen shallowVergleich mit den Kontrollen nur die Dateien Metadaten (mtime, Größe usw.). Bitte fügen Sie shallow=Falsein Ihrem Beispiel hinzu.
Famza
2
Zusätzlich werden die Ergebnisse zwischengespeichert .
Famza
11

Ich versuche immer zu vermeiden, Dateien auf die Festplatte zu schreiben, auch wenn es sich um einen temporären Ordner handelt, der meinen Tests gewidmet ist: Wenn Sie die Festplatte nicht berühren, werden Ihre Tests viel schneller, insbesondere wenn Sie häufig mit Dateien in Ihrem Code interagieren.

Angenommen, Sie haben diese "erstaunliche" Software in einer Datei namens 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.")

Um die write_to_fileMethode zu testen , können Sie so etwas in eine Datei im selben Ordner schreiben, der heißt 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.
quelle
3

Sie können die Inhaltsgenerierung von der Dateiverwaltung trennen. Auf diese Weise können Sie testen, ob der Inhalt korrekt ist, ohne mit temporären Dateien herumspielen und diese anschließend bereinigen zu müssen.

Wenn Sie eine Generatormethode schreiben , die jede Inhaltszeile liefert, können Sie eine Dateibehandlungsmethode verwenden, die eine Datei öffnet und file.writelines()mit der Zeilenfolge aufruft . Die beiden Methoden könnten sich sogar in derselben Klasse befinden: Testcode würde den Generator aufrufen, und Produktionscode würde den Dateihandler aufrufen.

Hier ist ein Beispiel, das alle drei Testmöglichkeiten zeigt. Normalerweise wählen Sie nur eine aus, je nachdem, welche Methoden für die zu testende Klasse verfügbar sind.

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
quelle
Könnten Sie dafür Beispielcode bereitstellen? Das klingt interessant.
Buhtz
1
Ich habe ein Beispiel für alle drei Ansätze hinzugefügt, @buhtz.
Don Kirkby
-1

Aufgrund von Vorschlägen habe ich Folgendes getan.

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)

Ich habe eine Unterklasse MyTestCase erstellt, da ich viele Funktionen habe, die Dateien lesen / schreiben müssen, sodass ich wirklich eine wiederverwendbare Assert-Methode haben muss. In meinen Tests würde ich MyTestCase anstelle von unittest.TestCase unterordnen.

Was denkst du darüber?

jan
quelle