Как выполнить модульное тестирование функций, записывающих файлы, используя python unittest
У меня есть функция Python, которая записывает выходной файл на диск.
Я хочу написать unit test для него, используя модуль Uittest для Python.
Как я могу утверждать равенство файлов? Я хотел бы получить ошибку, если содержимое файла отличается от ожидаемого + списка различий. Как и на выходе команды unix diff.
Есть ли официальный/рекомендуемый способ сделать это?
Ответы
Ответ 1
Самое простое - записать выходной файл, затем прочитать его содержимое, прочитать содержимое золотого (ожидаемого) файла и сравнить его с простым равенством строк. Если они совпадают, удалите выходной файл. Если они разные, поднимите утверждение.
Таким образом, когда тесты выполняются, каждый неудачный тест будет представлен выходным файлом, и вы можете использовать сторонний инструмент для их сравнения с золотыми файлами (Beyond Compare отлично подходит для этого).
Если вы действительно хотите предоставить свой собственный выход diff, помните, что у stdlib Python есть модуль difflib. Новая поддержка unittest в Python 3.1 включает метод assertMultiLineEqual
, который использует его для отображения diffs, аналогично этому:
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)
Ответ 2
Я предпочитаю, чтобы функции вывода явно принимали дескриптор файла (или файл-подобный объект), вместо того, чтобы принимать имя файла и открывать сам файл. Таким образом, я могу передать объект StringIO
в функцию вывода в моем unit test, затем .read()
содержимое обратно из этого StringIO
(после вызова .seek(0)
) и сравните с моим ожидаемым выходом.
Например, мы бы перевели код, подобный этому
##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")
для этого кода
##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")
Этот подход имеет дополнительное преимущество в том, чтобы сделать вашу функцию вывода более гибкой, если, например, вы решили, что не хотите писать в файл, но какой-то другой буфер, так как он будет принимать все объекты, подобные файлу.
Обратите внимание, что использование StringIO
предполагает, что содержимое тестового вывода может вписываться в основную память. Для очень большого вывода вы можете использовать временный файл (например, tempfile.SpooledTemporaryFile).
Ответ 3
import filecmp
Тогда
self.assertTrue(filecmp.cmp(path1, path2))
Ответ 4
Вы можете отделить создание контента от обработки файлов. Таким образом, вы можете проверить, что контент правильный, без необходимости возиться с временными файлами и после этого очищать их.
Если вы пишете метод генератора который дает каждую строку содержимого, тогда у вас может быть метод обработки файлов, который открывает файл и вызывает file.writelines()
с последовательностью строк. Эти два метода могут быть даже в одном классе: тестовый код вызовет генератор, а производственный код вызовет обработчик файла.
Вот пример, который показывает все три способа тестирования. Обычно вы просто выбираете один, в зависимости от того, какие методы доступны для тестируемого класса.
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)
Ответ 5
Я всегда стараюсь избегать записи файлов на диск, даже если это временная папка, предназначенная для моих тестов: не касаясь диска, ваши тесты значительно ускоряются, особенно если вы много взаимодействуете с файлами в вашем коде.
Предположим, у вас есть этот "удивительный" фрагмент программного обеспечения в файле с именем 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.")
Чтобы протестировать метод write_to_file
, вы можете записать что-то вроде этого в файл в той же папке с именем 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")
Ответ 6
Основываясь на предложениях, я сделал следующее.
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)
Я создал подкласс MyTestCase, так как у меня есть много функций, которые нужно читать/записывать файлы, поэтому мне действительно нужно иметь метод повторного использования assert. Теперь в моих тестах я бы подклассифицировал MyTestCase вместо unittest.TestCase.
Что вы думаете об этом?