Làm cách nào để tôi giả lập một mở được sử dụng trong câu lệnh with (sử dụng khung Mock trong Python)?


188

Làm cách nào để kiểm tra mã sau bằng giả (sử dụng giả, trang trí bản vá và các câu lệnh được cung cấp bởi khung Mock của Michael Foord ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()

@Daryl Spitzer: bạn có thể bỏ qua câu hỏi meta ("Tôi biết câu trả lời ...") Thật khó hiểu.
S.Lott

Trước đây khi tôi bỏ nó đi, mọi người đã phàn nàn rằng tôi đang trả lời câu hỏi của chính mình. Tôi sẽ thử chuyển câu trả lời của tôi.
Daryl Spitzer

1
@Daryl: Cách tốt nhất để tránh những phàn nàn về việc trả lời câu hỏi của chính mình, thường xuất phát từ những lo lắng về "nghiệp chướng", là đánh dấu câu hỏi và / hoặc trả lời là "wiki cộng đồng".
John Millikin

3
Nếu tôi trả lời câu hỏi của riêng bạn được coi là Karma Whoring, Câu hỏi thường gặp cần được làm rõ về điểm đó tôi nghĩ.
EBGreen

Câu trả lời:


131

Cách thức thực hiện điều này đã thay đổi trong mock 0.7.0, cuối cùng hỗ trợ chế tạo các phương thức giao thức python (phương thức ma thuật), đặc biệt là sử dụng MagicMock:

http://www.voidspace.org.uk/python/mock/magicmock.html

Một ví dụ về giả định mở như một trình quản lý bối cảnh (từ trang ví dụ trong tài liệu giả):

>>> open_name = '%s.open' % __name__
>>> with patch(open_name, create=True) as mock_open:
...     mock_open.return_value = MagicMock(spec=file)
...
...     with open('/some/path', 'w') as f:
...         f.write('something')
...
<mock.Mock object at 0x...>
>>> file_handle = mock_open.return_value.__enter__.return_value
>>> file_handle.write.assert_called_with('something')

Ồ Điều này có vẻ đơn giản hơn nhiều so với ví dụ về trình quản lý bối cảnh hiện tại tại voidspace.org.uk/python/mock/magicmock.html , điều này rõ ràng đặt __enter____exit__giả định các đối tượng - là cách tiếp cận sau đã lỗi thời hay vẫn còn hữu ích?
Brandon Rhodes

5
"Cách tiếp cận thứ hai" đang chỉ ra cách thực hiện mà không cần sử dụng MagicMock (tức là nó chỉ là một ví dụ về cách Mock hỗ trợ các phương thức ma thuật). Nếu bạn sử dụng MagicMock (như trên) thì nhậpthoát được cấu hình sẵn cho bạn.
fuzzyman

5
bạn có thể trỏ đến bài đăng trên blog của mình , nơi bạn giải thích chi tiết hơn tại sao / cách thức hoạt động
Coleue

9
Trong Python 3, 'tệp' không được xác định (được sử dụng trong thông số MagicMock), vì vậy tôi đang sử dụng io.IOBase thay thế.
Jonathan Hartley

1
Lưu ý: trong Python3, nội trang fileđã biến mất!
shoutuma

239

mock_openlà một phần của mockkhung và rất đơn giản để sử dụng. patchđược sử dụng làm bối cảnh trả về đối tượng được sử dụng để thay thế đối tượng đã vá: bạn có thể sử dụng nó để làm cho thử nghiệm của mình đơn giản hơn.

Python 3.x

Sử dụng builtinsthay vì __builtin__.

from unittest.mock import patch, mock_open
with patch("builtins.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Python 2.7

mockkhông phải là một phần của unittestvà bạn nên vá__builtin__

from mock import patch, mock_open
with patch("__builtin__.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Trường hợp trang trí

Nếu bạn sử dụng patchlàm trang trí bằng cách sử dụng mock_open()kết quả new patchcủa đối số thì có thể hơi lạ một chút.

Trong trường hợp này tốt hơn là sử dụng new_callable patchđối số và nhớ rằng mọi đối số bổ sung patchkhông sử dụng sẽ được chuyển sang new_callablechức năng như được mô tả trong patchtài liệu .

patch () lấy các đối số từ khóa tùy ý. Chúng sẽ được chuyển đến Mock (hoặc new_callable) khi xây dựng.

Ví dụ phiên bản trang trí cho Python 3.x là:

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_patch(mock_file):
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Hãy nhớ rằng trong trường hợp patchnày sẽ thêm đối tượng giả làm đối số của hàm kiểm tra của bạn.


Xin lỗi vì đã hỏi, có with patch("builtins.open", mock_open(read_data="data")) as mock_file:thể chuyển đổi thành cú pháp trang trí không? Tôi đã thử, nhưng tôi không chắc những gì tôi cần chuyển vào @patch("builtins.open", ...) làm đối số thứ hai.
imrek

1
@DrunkenMaster Cập nhật .. cảm ơn vì đã chỉ nó. Sử dụng trang trí không phải là chuyện nhỏ trong trường hợp này.
Michele Keyboardmico

Khốn kiếp Vấn đề của tôi là một chút phức tạp hơn (tôi đã để kênh return_valuecủa mock_openvào một đối tượng giả và khẳng định các mô hình thứ hai return_value), nhưng nó làm việc bằng cách thêm mock_opennhư new_callable.
imrek

1
@ArthurZopellaro hãy xem sixmô-đun để có một mockmô-đun nhất quán . Nhưng tôi không biết nếu nó cũng ánh xạ builtinstrong một mô-đun chung.
Michele Keyboardmico

1
Làm thế nào để bạn tìm thấy tên chính xác để vá? Tức là làm thế nào để bạn tìm thấy đối số đầu tiên cho @patch ('buildins.open' trong trường hợp này) cho một hàm tùy ý?
zenperttu

73

Với các phiên bản mới nhất của giả, bạn có thể sử dụng trình trợ giúp mock_open thực sự hữu ích :

mock_open (mock = Không, read_data = Không)

Một chức năng trợ giúp để tạo ra một giả để thay thế việc sử dụng mở. Nó hoạt động để mở được gọi trực tiếp hoặc được sử dụng như một trình quản lý bối cảnh.

Đối số giả là đối tượng giả để cấu hình. Nếu Không có (mặc định) thì MagicMock sẽ được tạo cho bạn, với API giới hạn ở các phương thức hoặc thuộc tính có sẵn trên tay cầm tệp tiêu chuẩn.

read_data là một chuỗi cho phương thức đọc của tệp xử lý trả về. Đây là một chuỗi rỗng theo mặc định.

>>> from mock import mock_open, patch
>>> m = mock_open()
>>> with patch('{}.open'.format(__name__), m, create=True):
...    with open('foo', 'w') as h:
...        h.write('some stuff')

>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

Làm thế nào để bạn kiểm tra nếu có nhiều .writecuộc gọi?
n611x007

1
@naxa Một cách là truyền từng tham số dự kiến ​​vào handle.write.assert_any_call(). Bạn cũng có thể sử dụng handle.write.call_args_listđể nhận từng cuộc gọi nếu đơn hàng quan trọng.
Rob Cutmore

m.return_value.write.assert_called_once_with('some stuff')là tốt hơn imo. Tránh đăng ký một cuộc gọi.
Ẩn danh

2
Thủ công khẳng định Mock.call_args_listlà an toàn hơn so với việc gọi bất kỳ Mock.assert_xxxphương thức nào. Nếu bạn đánh vần sai bất kỳ từ nào sau này, là thuộc tính của Mock, chúng sẽ luôn âm thầm vượt qua.
Jonathan Hartley

12

Để sử dụng mock_open cho một tệp đơn giản read()(đoạn mã mock_open ban đầu đã được cung cấp trên trang này được định hướng nhiều hơn để ghi):

my_text = "some text to return when read() is called on the file object"
mocked_open_function = mock.mock_open(read_data=my_text)

with mock.patch("__builtin__.open", mocked_open_function):
    with open("any_string") as f:
        print f.read()

Lưu ý theo tài liệu cho mock_open, điều này đặc biệt dành cho read(), vì vậy sẽ không hoạt động với các mẫu phổ biến như for line in f, chẳng hạn.

Sử dụng trăn 2.6.6 / mock 1.0.1


Có vẻ tốt, nhưng tôi không thể làm cho nó hoạt động với for line in opened_file:loại mã. Tôi đã thử trải nghiệm với StringIO có thể lặp lại thực hiện __iter__và sử dụng nó thay vì my_text, nhưng không có may mắn.
Evgen

@EvgeniiPuchkaryov Điều này đặc biệt hiệu quả read()vì vậy sẽ không hoạt động trong for line in opened_filetrường hợp của bạn ; Tôi đã chỉnh sửa bài đăng để làm rõ
jlb83

1
@EvgeniiPuchkaryov for line in f:hỗ trợ có thể đạt được bằng cách chế giễu giá trị trả về open()một đối tượng StringIO thay .
Iskar Jarak

1
Để làm rõ, hệ thống được thử nghiệm (SUT) trong ví dụ này là: with open("any_string") as f: print f.read()
Brad M

4

Câu trả lời hàng đầu là hữu ích nhưng tôi đã mở rộng về nó một chút.

Nếu bạn muốn đặt giá trị của đối tượng tệp của mình (phần ftrong as f) dựa trên các đối số được truyền cho open()đây, một cách để thực hiện:

def save_arg_return_data(*args, **kwargs):
    mm = MagicMock(spec=file)
    mm.__enter__.return_value = do_something_with_data(*args, **kwargs)
    return mm
m = MagicMock()
m.side_effect = save_arg_return_array_of_data

# if your open() call is in the file mymodule.animals 
# use mymodule.animals as name_of_called_file
open_name = '%s.open' % name_of_called_file

with patch(open_name, m, create=True):
    #do testing here

Về cơ bản, open()sẽ trả về một đối tượng và withsẽ gọi __enter__()đối tượng đó.

Để chế nhạo đúng cách, chúng ta phải giả lập open()để trả về một đối tượng giả. Đối tượng giả đó sau đó sẽ mô phỏng __enter__()cuộc gọi trên nó ( MagicMocksẽ thực hiện điều này cho chúng tôi) để trả về đối tượng dữ liệu / tệp giả mà chúng tôi muốn (do đó mm.__enter__.return_value). Làm điều này với 2 giả theo cách trên cho phép chúng tôi nắm bắt các đối số được truyền đến open()và chuyển chúng sang do_something_with_dataphương thức của chúng tôi .

Tôi đã chuyển toàn bộ tệp giả thành một chuỗi open()và tôi do_something_with_datatrông như thế này:

def do_something_with_data(*args, **kwargs):
    return args[0].split("\n")

Điều này biến đổi chuỗi thành một danh sách để bạn có thể thực hiện như sau với một tệp bình thường:

for line in file:
    #do action

Nếu mã đang được kiểm tra xử lý tệp theo một cách khác, ví dụ bằng cách gọi hàm của nó là "readline", bạn có thể trả về bất kỳ đối tượng giả nào bạn muốn trong hàm "do_s Something_with_data" với các thuộc tính mong muốn.
dùng3289695

Có cách nào để tránh chạm vào __enter__? Nó chắc chắn trông giống như một hack hơn là một cách được đề xuất.
imrek

nhập là cách các trình quản lý conext như open () được viết. Mocks thường sẽ có một chút hacky ở chỗ bạn cần truy cập vào các công cụ "riêng tư" để chế nhạo, nhưng việc nhập vào đây không hoàn toàn là hacky imo
theannouncer

3

Tôi có thể hơi trễ trò chơi, nhưng điều này hiệu quả với tôi khi gọi opentrong một mô-đun khác mà không phải tạo một tệp mới.

kiểm tra

import unittest
from mock import Mock, patch, mock_open
from MyObj import MyObj

class TestObj(unittest.TestCase):
    open_ = mock_open()
    with patch.object(__builtin__, "open", open_):
        ref = MyObj()
        ref.save("myfile.txt")
    assert open_.call_args_list == [call("myfile.txt", "wb")]

MyObj.py

class MyObj(object):
    def save(self, filename):
        with open(filename, "wb") as f:
            f.write("sample text")

Bằng cách vá openchức năng bên trong __builtin__mô-đun thành của tôi mock_open(), tôi có thể giả định ghi vào một tệp mà không cần tạo.

Lưu ý: Nếu bạn đang sử dụng một mô-đun sử dụng cython hoặc chương trình của bạn phụ thuộc vào cython theo bất kỳ cách nào, bạn sẽ cần nhập mô-đun của cython__builtin__ bằng cách bao gồm import __builtin__ở đầu tệp của bạn. Bạn sẽ không thể chế giễu phổ quát __builtin__nếu bạn đang sử dụng cython.


Một biến thể của cách tiếp cận này có hiệu quả đối với tôi, vì phần lớn mã được thử nghiệm nằm trong các mô-đun khác như được hiển thị ở đây. Tôi đã cần phải chắc chắn để thêm import __builtin__vào mô-đun thử nghiệm của tôi. Bài viết này đã giúp làm rõ lý do tại sao kỹ thuật này hoạt động tốt như nó: ichimonji10.name/blog/6
killthrush

0

Để vá hàm open () tích hợp với unittest:

Điều này làm việc cho một bản vá để đọc một cấu hình json.

class ObjectUnderTest:
    def __init__(self, filename: str):
        with open(filename, 'r') as f:
            dict_content = json.load(f)

Đối tượng bị giả là đối tượng io.TextIOWrapper được trả về bởi hàm open ()

@patch("<src.where.object.is.used>.open",
        return_value=io.TextIOWrapper(io.BufferedReader(io.BytesIO(b'{"test_key": "test_value"}'))))
    def test_object_function_under_test(self, mocker):

0

Nếu bạn không cần thêm bất kỳ tập tin nào, bạn có thể trang trí phương pháp kiểm tra:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
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.