出力ファイルをディスクに書き込むPython関数があります。
Pythonのunittest
モジュールを使用してユニットテストを作成したいと思います。
ファイルの同等性をどのように主張する必要がありますか?ファイルの内容が予想されるものと異なる場合、エラーが発生します+違いのリスト。Unixのdiffコマンドの出力と同じです。
それを行うための公式または推奨される方法はありますか?
回答:
最も簡単なことは、出力ファイルを書き込んでからその内容を読み取り、ゴールド(予期される)ファイルの内容を読み取り、それらを単純な文字列の同等性と比較することです。それらが同じである場合は、出力ファイルを削除します。それらが異なる場合は、アサーションを発生させます。
このように、テストが完了すると、失敗したすべてのテストが出力ファイルで表され、サードパーティのツールを使用してゴールドファイルと比較できます(Beyond Compareはこれに最適です)。
本当に独自のdiff出力を提供したい場合は、Pythonstdlibにdifflibモジュールがあることに注意してください。Python 3.1の新しいユニットテストサポートにはassertMultiLineEqual
、次のように、それを使用して差分を表示するメソッドが含まれています。
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)
私は、ファイル名を受け入れてファイル自体を開くのではなく、出力関数がファイルハンドル(またはファイルのようなオブジェクト)を明示的に受け入れるようにすることを好みます。このようにして、単体テストでオブジェクトを出力関数に渡し、そのオブジェクトからコンテンツを(呼び出し後に)戻し、期待される出力と比較することができます。StringIO
.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)を使用できます。
テスト専用の一時フォルダーであっても、常にディスクへのファイルの書き込みは避けようとしています。実際にディスクに触れないことで、特にコード内のファイルを頻繁に操作する場合に、テストがはるかに高速になります。
この「驚くべき」ソフトウェアが次のファイルに含まれているとします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")
コンテンツの生成をファイルの処理から分離することができます。そうすれば、一時ファイルをいじったり、後でクリーンアップしたりすることなく、コンテンツが正しいことをテストできます。
コンテンツの各行を生成するジェネレーターメソッドを作成する場合は、ファイルを開いて呼び出すファイル処理メソッドを作成できます。file.writelines()
て一連の行。2つのメソッドが同じクラスにある場合もあります。テストコードはジェネレーターを呼び出し、プロダクションコードはファイルハンドラーを呼び出します。
これは、テストする3つの方法すべてを示す例です。通常、テストするクラスで使用できるメソッドに応じて、1つを選択します。
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)
提案に基づいて、私は次のことを行いました。
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メソッドが本当に必要です。私のテストでは、unittest.TestCaseの代わりにMyTestCaseをサブクラス化します。
あなたはそれについてどう思いますか?
open
説明されているように呼び出しをモックすることですこのページの他の回答では、unittest.mock
(Enrico Mからの回答を参照)