Python의 'unittest'를 사용하여 파일을 작성하는 함수의 단위 테스트를 수행하는 방법


83

출력 파일을 디스크에 쓰는 Python 함수가 있습니다.

Python unittest모듈을 사용하여 단위 테스트를 작성하고 싶습니다 .

파일의 동등성을 어떻게 주장해야합니까? 파일 내용이 예상 내용과 다른 경우 오류가 발생하고 + 차이점 목록이 표시됩니다. Unix diff 명령 의 출력에서와 같습니다 .

공식 또는 권장 방법이 있습니까?

답변:


50

가장 간단한 방법은 출력 파일을 작성한 다음 해당 내용을 읽고 금 (예상) 파일의 내용을 읽고 단순한 문자열 동등성과 비교하는 것입니다. 동일한 경우 출력 파일을 삭제하십시오. 다른 경우 주장을 제기하십시오.

이런 식으로 테스트가 완료되면 실패한 모든 테스트가 출력 파일로 표시되고 타사 도구를 사용하여 골드 파일과 비교할 수 있습니다 ( Beyond Compare 는이 기능이 훌륭합니다).

자신 만의 diff 출력을 제공하고 싶다면 Python stdlib에 difflib 모듈이 있음을 기억하십시오. Python 3.1의 새로운 unittest 지원에는 다음 assertMultiLineEqual과 유사한 diff를 표시하는 데 사용 하는 메서드가 포함되어 있습니다 .

    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)

아니요, 전반적으로 가장 좋은 방법은 느리고 오류가 발생하기 쉬운 파일에 쓰지 않는 것입니다 (prod env는 Windows 대 OSX와 같이 test / CI env와 완전히 다를 수 있음). 대신 open설명 된대로 호출을 조롱하는 것입니다. 이 페이지의 다른 답변에서 사용 unittest.mock(엔리코 M에서 답을 참조)
에릭

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

이 접근 방식은 예를 들어 파일에 쓰지 않고 다른 버퍼에 모든 파일 류 객체를 허용하기 때문에 출력 기능을 더 유연하게 만드는 추가 이점이 있습니다.

using StringIO은 테스트 출력의 내용이 주 메모리에 맞을 수 있다고 가정합니다. 매우 큰 출력의 경우 임시 파일 접근 방식 (예 : tempfile.SpooledTemporaryFile )을 사용할 수 있습니다 .


2
이것은 디스크에 파일을 쓰는 것보다 낫습니다. 많은 단위 테스트를 실행하는 경우 IO to disk는 모든 종류의 문제를 유발하며 특히 정리를 시도합니다. 디스크에 쓰는 테스트, 작성된 파일을 삭제하는 tearDown이 있습니다. 테스트는 한 번에 하나씩 제대로 작동 한 다음 모두 실행하면 실패합니다. 적어도 Win 컴퓨터의 Visual Studio 및 PyTools와 함께. 또한 속도.
srock

1
이것은 별도의 기능을 테스트하는 좋은 솔루션이지만 프로그램이 제공하는 실제 인터페이스 (예 : CLI 도구)를 테스트 할 때는 여전히 문제가됩니다.
Joost

1
유니 코드 인수가 예상 'STR'가지고 : 형식 오류 : 나는 오류가 발생했습니다
cn123h

파일 단위로 분할 된 쪽모이 세공 데이터 세트를 걷고 읽기위한 단위 테스트를 작성하려고하기 때문에 여기에 왔습니다. 이를 위해서는 파일 경로를 구문 분석하여 키 / 값 쌍을 가져 와서 결과적으로 생성되는 Pandas DataFrame에 적절한 파티션 값을 할당해야합니다. 버퍼에 쓰는 것은 좋지만 파티션 값을 구문 분석하는 기능을 제공하지 않습니다.
PMende

1
@PMende 실제 파일 시스템과의 상호 작용이 필요한 API로 작업하는 것 같습니다. 단위 테스트가 항상 적절한 수준의 테스트는 아닙니다. 단위 테스트 수준에서 코드의 모든 부분을 테스트하지 않아도됩니다. 적절한 경우 통합 또는 시스템 테스트도 사용해야합니다. 그러나 이러한 부분을 포함하고 가능하면 경계 사이에 간단한 값만 전달하십시오. youtube.com/watch?v=eOYal8elnZk
gotgenes

20
import filecmp

그때

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

2
하여 기본 이 수행하는shallow 비교하는 검사 파일 만 메타 데이터 (mtime에, 크기 등). 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() 일련의 줄로 . 두 메서드는 동일한 클래스에있을 수도 있습니다. 테스트 코드는 생성기를 호출하고 프로덕션 코드는 파일 핸들러를 호출합니다.

다음은 세 가지 테스트 방법을 모두 보여주는 예입니다. 일반적으로 테스트 할 클래스에서 사용할 수있는 메서드에 따라 하나만 선택합니다.

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

1
세 가지 접근 방식 모두 @buhtz에 대한 예제를 추가했습니다.
Don Kirkby

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

파일을 읽고 쓰는 데 필요한 많은 함수가 있으므로 다시 사용할 수있는 assert 메서드가 필요하므로 하위 클래스 MyTestCase를 만들었습니다. 이제 테스트에서 unittest.TestCase 대신 MyTestCase를 하위 클래스로 지정합니다.

당신이 그것에 대해 어떻게 생각하십니까?


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.