Pythonの「unittest」を使用してファイルを書き込む関数のユニットテストを行う方法


83

出力ファイルをディスクに書き込むPython関数があります。

Pythonのunittestモジュールを使用してユニットテストを作成したいと思います。

ファイルの同等性をどのように主張する必要がありますか?ファイルの内容が予想されるものと異なる場合、エラーが発生します+違いのリスト。Unixのdiffコマンドの出力と同じです。

それを行うための公式または推奨される方法はありますか?

回答:


50

最も簡単なことは、出力ファイルを書き込んでからその内容を読み取り、ゴールド(予期される)ファイルの内容を読み取り、それらを単純な文字列の同等性と比較することです。それらが同じである場合は、出力ファイルを削除します。それらが異なる場合は、アサーションを発生させます。

このように、テストが完了すると、失敗したすべてのテストが出力ファイルで表され、サードパーティのツールを使用してゴールドファイルと比較できます(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)

いいえ、全体として最善の方法は、速度が遅くエラーが発生しやすいファイルに書き込むことではなく(prodenvはWindowsとOSXのようにtest / CI envとは完全に異なる場合があります)、代わりにopen説明されているように呼び出しをモックすることですこのページの他の回答では、unittest.mock(Enrico Mからの回答を参照)
Eric

71

私は、ファイルを受け入れてファイル自体を開くのではなく、出力関数がファイルハンドル(またはファイルのようなオブジェクト)を明示的に受け入れるようにすることを好みます。このようにして、単体テストでオブジェクトを出力関数に渡し、そのオブジェクトからコンテンツを(呼び出し後に)戻し、期待される出力と比較することができます。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)を使用できます。


2
これは、ファイルをディスクに書き込むよりも優れています。大量の単体テストを実行している場合、ディスクへのIOはあらゆる種類の問題を引き起こし、特にそれらをクリーンアップしようとします。ディスクへの書き込みテストがあり、tearDownは書き込まれたファイルを削除しました。テストは一度に1つずつ正常に機能し、すべて実行すると失敗します。少なくとも、WinマシンのVisualStudioとPyToolsでは。また、スピード。
srock 2015

1
これは別々の機能をテストするための良い解決策ですが、あなたのプログラムが提供していることを実際のインターフェース(例えばA CLIツール)..テストするとき、それはまだ面倒である
Joostの

1
私はエラーを得た:例外TypeError:ユニコードの引数が期待されるが、「STR」を得た
cn123h

パーティション化された寄木細工のデータセットをファイルごとに歩いたり読んだりするための単体テストを作成しようとしているため、ここに来ました。これには、ファイルパスを解析してキーと値のペアを取得し、パーティションの適切な値を(最終的に)結果のパンダDataFrameに割り当てる必要があります。バッファへの書き込みは優れていますが、パーティション値を解析する機能はありません。
PMende

1
@PMende実際のファイルシステムとの対話が必要なAPIを使用しているようです。単体テストは、必ずしも適切なレベルのテストであるとは限りません。単体テストのレベルでコードのすべての部分をテストしなくてもかまいません。必要に応じて、統合テストまたはシステムテストも使用する必要があります。ただし、これらの部分を含めるようにして、可能な限り境界間に単純な値のみを渡してください。参照youtube.com/watch?v=eOYal8elnZk
gotgenes

20
import filecmp

次に

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

2
デフォルトこれはありませんshallow比較したチェックのみファイルのメタデータ(最終変更時刻、サイズ、など)。shallow=False例を追加してください。
famzah

2
さらに、結果はキャッシュされます。
famzah

12

テスト専用の一時フォルダーであっても、常にディスクへのファイルの書き込みは避けようとしています。実際にディスクに触れないことで、特にコード内のファイルを頻繁に操作する場合に、テストがはるかに高速になります。

この「驚くべき」ソフトウェアが次のファイルに含まれているとします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")

3

コンテンツの生成をファイルの処理から分離することができます。そうすれば、一時ファイルをいじったり、後でクリーンアップしたりすることなく、コンテンツが正しいことをテストできます。

コンテンツの各行を生成するジェネレーターメソッドを作成する場合は、ファイルを開いて呼び出すファイル処理メソッドを作成できます。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)

そのためのサンプルコードを提供できますか?それは面白そう。
buhtz 2017年

1
3つのアプローチすべての例@buhtzを追加しました。
ドンカークビー2017年

-1

提案に基づいて、私は次のことを行いました。

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をサブクラス化します。

あなたはそれについてどう思いますか?


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.