Cách thực hiện kiểm tra đơn vị các chức năng viết tệp bằng python unittest


81

Tôi có một hàm Python ghi tệp đầu ra vào đĩa.

Tôi muốn viết một bài kiểm tra đơn vị cho nó bằng cách sử dụng mô-đun đơn nhất của Python.

Tôi nên khẳng định sự bình đẳng của các tệp như thế nào? Tôi muốn gặp lỗi nếu nội dung tệp khác với dự kiến ​​+ danh sách các điểm khác biệt. Như trong đầu ra của lệnh unix diff.

Có cách nào chính thức / được khuyến nghị để làm điều đó không?

Câu trả lời:


48

Điều đơn giản nhất là ghi tệp đầu ra, sau đó đọc nội dung của nó, đọc nội dung của tệp vàng (dự kiến) và so sánh chúng với đẳng thức chuỗi đơn giản. Nếu chúng giống nhau, hãy xóa tệp đầu ra. Nếu chúng khác nhau, hãy khẳng định.

Bằng cách này, khi các bài kiểm tra được thực hiện, mọi bài kiểm tra không thành công sẽ được thể hiện bằng một tệp đầu ra và bạn có thể sử dụng công cụ của bên thứ 3 để khác biệt chúng với các tệp vàng (Beyond Compare là tuyệt vời cho điều này).

Nếu bạn thực sự muốn cung cấp đầu ra khác biệt của riêng mình, hãy nhớ rằng Python stdlib có mô-đun difflib. Hỗ trợ mới nhất trong Python 3.1 bao gồm một assertMultiLineEqualphương thức sử dụng nó để hiển thị các điểm khác biệt, tương tự như sau:

    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)

70

Tôi muốn các hàm đầu ra chấp nhận một cách rõ ràng một xử lý tệp (hoặc đối tượng giống tệp ) hơn là chấp nhận tên tệp và tự mở tệp. Bằng cách này, tôi có thể chuyển một StringIOđối tượng đến hàm đầu ra trong bài kiểm tra đơn vị của mình, sau đó .read()nội dung trở lại từ StringIOđối tượng đó (sau một .seek(0)cuộc gọi) và so sánh với đầu ra mong đợi của tôi.

Ví dụ: chúng tôi sẽ chuyển đổi mã như thế này

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

viết mã như thế này

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

Cách tiếp cận này có thêm lợi ích là làm cho chức năng đầu ra của bạn linh hoạt hơn nếu chẳng hạn, bạn quyết định không muốn ghi vào tệp mà là một số vùng đệm khác, vì nó sẽ chấp nhận tất cả các đối tượng giống tệp.

Lưu ý rằng sử dụng StringIOgiả định nội dung của đầu ra kiểm tra có thể vừa với bộ nhớ chính. Đối với đầu ra rất lớn, bạn có thể sử dụng cách tiếp cận tệp tạm thời (ví dụ: tempfile.SpooledTemporaryFile ).


2
Điều này tốt hơn sau đó ghi một tệp vào đĩa. Nếu bạn đang chạy hàng tấn unittest, IO to disk gây ra tất cả các loại vấn đề, đặc biệt là cố gắng dọn dẹp chúng. Tôi đã có thử nghiệm ghi vào đĩa, xé toạc xóa các tệp đã ghi. Các thử nghiệm sẽ hoạt động tốt tại một thời điểm, sau đó thất bại khi Chạy tất cả. Ít nhất là với Visual Studio và PyTools trên máy Win. Ngoài ra, tốc độ.
srock

1
Mặc dù đây là một giải pháp tốt để kiểm tra các chức năng riêng biệt, nhưng vẫn còn rắc rối khi kiểm tra giao diện thực tế mà chương trình của bạn cung cấp (ví dụ: một công cụ CLI) ..
Joost

1
Tôi đã nhận lỗi: Lỗi Loại: Lập luận unicode mong đợi, có 'str'
cn123h

Tôi đến đây vì tôi đang cố gắng viết các bài kiểm tra đơn vị để đi bộ và đọc tập dữ liệu sàn gỗ được phân vùng theo từng tệp. Điều này yêu cầu phân tích cú pháp đường dẫn tệp để có được các cặp khóa / giá trị để gán giá trị thích hợp của một phân vùng cho (cuối cùng) là DataFrame gấu trúc kết quả. Việc ghi vào bộ đệm, mặc dù tốt, nhưng không cho tôi khả năng phân tích cú pháp cho các giá trị phân vùng.
PMende

1
@PMende Có vẻ như bạn đang làm việc với một API cần tương tác với hệ thống tệp thực tế. Kiểm thử đơn vị không phải lúc nào cũng là mức kiểm tra thích hợp. Bạn có thể không kiểm tra tất cả các phần mã của mình ở cấp độ kiểm tra đơn vị; kiểm tra tích hợp hoặc hệ thống cũng nên được sử dụng khi thích hợp. Tuy nhiên, hãy cố gắng chứa những phần đó và chỉ chuyển các giá trị đơn giản giữa các ranh giới bất cứ khi nào có thể. Xem youtube.com/watch?v=eOYal8elnZk
gotgenes

20
import filecmp

Sau đó

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

2
Theo mặc định, điều này thực hiện một shallowso sánh chỉ kiểm tra siêu dữ liệu tệp (mtime, kích thước, v.v.). Vui lòng thêm shallow=Falsevào ví dụ của bạn.
famzah

2
Ngoài ra, kết quả được lưu vào bộ nhớ đệm .
famzah

11

Tôi luôn cố gắng tránh ghi tệp vào đĩa, ngay cả khi đó là thư mục tạm thời dành riêng cho các bài kiểm tra của tôi: việc không thực sự chạm vào đĩa sẽ làm cho các bài kiểm tra của bạn nhanh hơn nhiều, đặc biệt nếu bạn tương tác nhiều với các tệp trong mã của mình.

Giả sử bạn có phần mềm "tuyệt vời" này trong một tệp có tên 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.")

Để kiểm tra write_to_filephương pháp, bạn có thể viết một cái gì đó như thế này vào một tệp trong cùng một thư mục có tên 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

Bạn có thể tách việc tạo nội dung khỏi việc xử lý tệp. Bằng cách đó, bạn có thể kiểm tra xem nội dung có chính xác không mà không cần phải xáo trộn các tệp tạm thời và dọn dẹp chúng sau đó.

Nếu bạn viết một phương thức trình tạo tạo ra từng dòng nội dung, thì bạn có thể có một phương thức xử lý tệp để mở một tệp và gọi file.writelines()với chuỗi các dòng. Hai phương thức thậm chí có thể nằm trên cùng một lớp: mã kiểm tra sẽ gọi trình tạo và mã sản xuất sẽ gọi trình xử lý tệp.

Đây là một ví dụ cho thấy cả ba cách để kiểm tra. Thông thường, bạn sẽ chỉ chọn một, tùy thuộc vào phương pháp nào có sẵn trên lớp để kiểm tra.

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)

Bạn có thể cung cấp mã ví dụ cho điều đó? Nghe thú vị.
buhtz

1
Tôi đã thêm một ví dụ cho cả ba cách tiếp cận, @buhtz.
Don Kirkby

-1

Dựa trên những gợi ý tôi đã làm như sau.

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)

Tôi đã tạo một lớp con MyTestCase vì tôi có rất nhiều chức năng cần đọc / ghi tệp nên tôi thực sự cần có phương thức xác nhận có thể sử dụng lại. Bây giờ trong các bài kiểm tra của tôi, tôi sẽ phân lớp MyTestCase thay vì unittest.TestCase.

Bạn nghĩ gì về nó?


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.