Jak przeprowadzić testy jednostkowe funkcji zapisujących pliki przy użyciu funkcji „unittest” w języku Python

83

Mam funkcję Pythona, która zapisuje plik wyjściowy na dysku.

Chcę napisać dla niego test jednostkowy przy użyciu unittestmodułu Pythona .

Jak potwierdzić równość plików? Chciałbym otrzymać błąd, jeśli zawartość pliku różni się od oczekiwanej + lista różnic. Jak na wyjściu polecenia Unix diff .

Czy jest na to oficjalny lub zalecany sposób?

sty
źródło

Odpowiedzi:

50

Najprościej jest napisać plik wyjściowy, a następnie przeczytać jego zawartość, odczytać zawartość złotego (oczekiwanego) pliku i porównać je z prostą równością ciągów. Jeśli są takie same, usuń plik wyjściowy. Jeśli są różne, zgłoś twierdzenie.

W ten sposób, po zakończeniu testów, każdy nieudany test zostanie przedstawiony w pliku wyjściowym i możesz użyć narzędzia innej firmy, aby porównać je ze złotymi plikami ( Beyond Compare jest do tego wspaniały).

Jeśli naprawdę chcesz udostępnić własne wyjście diff, pamiętaj, że stdlib Pythona ma moduł difflib. Nowa obsługa unittest w Pythonie 3.1 zawiera assertMultiLineEqualmetodę, która używa jej do wyświetlania różnic, podobnie do tej:

    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
źródło
nie, ogólnie najlepszym sposobem jest nie zapisywanie w pliku, który może być powolny i podatny na błędy (prod env może być zupełnie inny niż test / CI env, jak Windows kontra OSX), ale zamiast tego udawać wywołanie openzgodnie z opisem w innych odpowiedziach na tej stronie, używając unittest.mock(patrz odpowiedź od Enrico M)
Eric
71

Wolę, aby funkcje wyjściowe wyraźnie akceptowały uchwyt pliku (lub obiekt podobny do pliku ), zamiast akceptować nazwę pliku i same otwierać plik. W ten sposób mogę przekazać StringIOobiekt do funkcji wyjściowej w moim teście jednostkowym, a następnie .read()zawartość z powrotem z tego StringIOobiektu (po .seek(0)wywołaniu) i porównać z oczekiwanymi wynikami.

Na przykład moglibyśmy zmienić kod w ten sposób

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

kodować w ten sposób

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

Takie podejście ma tę dodatkową zaletę, że sprawia, że ​​funkcja wyjściowa jest bardziej elastyczna, jeśli na przykład zdecydujesz, że nie chcesz zapisywać do pliku, ale do innego bufora, ponieważ akceptuje on wszystkie obiekty podobne do plików.

Zauważ, że użycie StringIOzakłada, że ​​zawartość wyjścia testowego zmieści się w pamięci głównej. W przypadku bardzo dużych danych wyjściowych można użyć metody pliku tymczasowego (np. Tempfile.SpooledTemporaryFile ).

gotgenes
źródło
2
Jest to lepsze niż zapisanie pliku na dysku. Jeśli uruchamiasz mnóstwo unittestów, IO na dysk powoduje różnego rodzaju problemy, szczególnie próby ich wyczyszczenia. Miałem testy zapisu na dysk, tearDown kasowania zapisanych plików. Testy działałyby poprawnie po jednym na raz, a następnie kończyłyby się niepowodzeniem po uruchomieniu wszystkich. Przynajmniej z Visual Studio i PyTools na maszynie Win. Również prędkość.
srock
1
Chociaż jest to przyjemne rozwiązanie do testowania oddzielnych funkcji, nadal jest kłopotliwe podczas testowania interfejsu, który zapewnia twój program (np. Narzędzie CLI).
Joost
1
Wystąpił
Przyszedłem tutaj, ponieważ próbuję napisać testy jednostkowe do chodzenia i odczytywania podzielonych na partycje zestawów danych parkietu plik po pliku. Wymaga to przeanalizowania ścieżki pliku w celu uzyskania par klucz / wartość w celu przypisania odpowiedniej wartości partycji do (ostatecznie) wynikowej pandy DataFrame. Zapisywanie do bufora, choć przyjemne, nie daje mi możliwości analizowania wartości partycji.
PMende
1
@PMende Wygląda na to, że pracujesz z API, które wymaga interakcji z rzeczywistym systemem plików. Testy jednostkowe nie zawsze są odpowiednim poziomem testowania. Dobrze jest nie testować wszystkich części kodu na poziomie testów jednostkowych; W stosownych przypadkach należy również zastosować testy integracyjne lub systemowe. Staraj się jednak zawrzeć te części i jeśli to możliwe, przekazuj tylko proste wartości między granicami. Zobacz youtube.com/watch?v=eOYal8elnZk
gotgenes
20
import filecmp

Następnie

self.assertTrue(filecmp.cmp(path1, path2))
tbc0
źródło
2
Przez domyślnie to robi shallowporównanie który sprawdza tylko metadanych plików (mtime, rozmiar, itp). Dodaj shallow=Falseswój przykład.
famzah
2
Dodatkowo wyniki są buforowane .
famzah
12

Zawsze staram się unikać zapisywania plików na dysku, nawet jeśli jest to folder tymczasowy przeznaczony na moje testy: faktyczne nie dotykanie dysku znacznie przyspiesza testy, zwłaszcza jeśli w kodzie występuje dużo interakcji z plikami.

Załóżmy, że masz to „niesamowite” oprogramowanie w pliku o nazwie 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.")

Aby przetestować write_to_filemetodę, możesz napisać coś takiego w pliku w tym samym folderze o nazwie 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.
źródło
3

Możesz oddzielić generowanie treści od obsługi plików. W ten sposób możesz sprawdzić, czy zawartość jest poprawna, bez konieczności majstrowania przy plikach tymczasowych i późniejszego ich czyszczenia.

Jeśli napiszesz metodę generatora, która zwraca każdy wiersz treści, możesz mieć metodę obsługi plików, która otwiera plik i wywołujefile.writelines() sekwencję wierszy. Te dwie metody mogą nawet należeć do tej samej klasy: kod testowy wywoływałby generator, a kod produkcyjny wywoływałby procedurę obsługi plików.

Oto przykład, który pokazuje wszystkie trzy sposoby testowania. Zwykle wystarczy wybrać jedną, w zależności od tego, jakie metody są dostępne w klasie do przetestowania.

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
źródło
Czy możesz podać przykładowy kod? To brzmi interesująco.
buhtz
1
Dodałem przykład dla wszystkich trzech podejść, @buhtz.
Don Kirkby
-1

Na podstawie sugestii wykonałem następujące czynności.

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)

Stworzyłem podklasę MyTestCase, ponieważ mam wiele funkcji, które muszą odczytywać / zapisywać pliki, więc naprawdę potrzebuję metody assert wielokrotnego użytku. Teraz w moich testach podklasowałbym MyTestCase zamiast unittest.TestCase.

Co o tym myślisz?

sty
źródło