ฉันจะเลียนแบบการเปิดที่ใช้ในคำสั่ง with (โดยใช้กรอบการจำลองใน Python) ได้อย่างไร?


188

ฉันจะทดสอบโค้ดต่อไปนี้ด้วย mocks ได้อย่างไร (โดยใช้ mocks เครื่องมือตกแต่งแพทช์และรักษาการณ์ที่จัดทำโดยกรอบงานจำลองของ Michael Foord )

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

@Daryl Spitzer: คุณช่วยออกเมตาคำถาม ("ฉันรู้คำตอบ ... ") มันสับสน
S.Lott

ในอดีตเมื่อฉันทิ้งมันไปผู้คนบ่นว่าฉันตอบคำถามของตัวเอง ฉันจะลองย้ายไปที่คำตอบของฉัน
Daryl Spitzer

1
@Daryl: วิธีที่ดีที่สุดในการหลีกเลี่ยงการร้องเรียนเกี่ยวกับการตอบคำถามของตัวเองซึ่งมักเกิดจากความกังวลเรื่อง "กรรมโสเภณี" คือการทำเครื่องหมายคำถามและ / หรือคำตอบว่าเป็น "ชุมชนวิกิ"
John Millikin

3
หากตอบคำถามของคุณเองถือว่าเป็น Karma Whoring คำถามที่พบบ่อยควรชี้แจงในจุดนั้นฉันคิดว่า
EBGreen

คำตอบ:


132

วิธีการทำเช่นนี้มีการเปลี่ยนแปลงใน mock 0.7.0 ซึ่งในที่สุดก็รองรับการเยาะเย้ยวิธีการโพรโทคอลหลาม

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

ตัวอย่างของการเยาะเย้ยเปิดเป็นตัวจัดการบริบท (จากหน้าตัวอย่างในเอกสารจำลอง):

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

ว้าว! สิ่งนี้ดูง่ายกว่าตัวอย่าง context-manager ในปัจจุบันที่voidspace.org.uk/python/mock/magicmock.htmlซึ่งกำหนดอย่างชัดเจน__enter__และ__exit__จำลองวัตถุเช่นกัน - แนวทางหลังล้าสมัยหรือยังมีประโยชน์หรือไม่
แบรนดอนโรดส์

6
"วิธีการหลัง" แสดงให้เห็นว่าจะทำอย่างไรโดยไม่ต้องใช้ MagicMock (นั่นเป็นเพียงตัวอย่างของวิธีการที่ Mock รองรับวิธีการทางเวทมนตร์) หากคุณใช้ MagicMock (ดังที่แสดงไว้ด้านบน) การป้อนและออกจะได้รับการกำหนดค่าไว้ล่วงหน้าสำหรับคุณ
fuzzyman

5
คุณสามารถชี้ไปที่โพสต์บล็อกของคุณที่คุณอธิบายรายละเอียดเพิ่มเติมว่าทำไม / วิธีการทำงาน
Rodriguez

9
ใน Python 3 ไม่ได้กำหนด 'ไฟล์' (ใช้ในข้อมูลจำเพาะ MagicMock) ดังนั้นฉันจึงใช้ io.IOBase แทน
Jonathan Hartley

1
หมายเหตุ: ใน Python3 builtin fileหายไปแล้ว!
exhuma

239

mock_openเป็นส่วนหนึ่งของmockกรอบงานและใช้งานง่ายมาก patchใช้เป็นบริบทส่งคืนวัตถุที่ใช้ในการแทนที่หนึ่ง patched: คุณสามารถใช้มันเพื่อให้การทดสอบของคุณง่ายขึ้น

Python 3.x

ใช้แทนbuiltins__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

mockไม่ได้เป็นส่วนหนึ่งunittestและคุณควรแก้ไข__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")

กรณีมัณฑนากร

ถ้าคุณจะใช้patchเป็นมัณฑนากรโดยใช้mock_open()ผลลัพธ์ของเนื่องจากnew patchอาร์กิวเมนต์อาจแปลกไปหน่อย

ในกรณีนี้จะดีกว่าที่จะใช้new_callable patch's อาร์กิวเมนต์และจำไว้ว่าทุกข้อโต้แย้งพิเศษที่patchไม่ได้ใช้งานจะถูกส่งผ่านไปยังnew_callableฟังก์ชั่นที่อธิบายไว้ในเอกสารpatch

patch () รับข้อโต้แย้งคำหลักโดยพลการ สิ่งเหล่านี้จะถูกส่งไปยังจำลอง (หรือ new_callable) ในการก่อสร้าง

เช่นรุ่นตกแต่งสำหรับPython 3.xคือ:

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

โปรดจำไว้ว่าในกรณีนี้patchจะเพิ่มวัตถุจำลองเป็นอาร์กิวเมนต์ของคุณทดสอบฟังก์ชั่น


ขออภัยที่ขอให้with patch("builtins.open", mock_open(read_data="data")) as mock_file:แปลงเป็นไวยากรณ์มัณฑนากร? ฉันพยายามแล้ว แต่ฉันไม่แน่ใจว่าสิ่งที่ฉันต้องผ่าน@patch("builtins.open", ...) เป็นข้อโต้แย้งที่สอง
imrek

1
@DrunkenMaster Updateted .. ขอบคุณสำหรับการชี้ การใช้มัณฑนากรไม่ได้สำคัญในกรณีนี้
Michele d'Amico

Grazie! ปัญหาของฉันเป็นบิตที่ซับซ้อนมากขึ้น (ผมต้องช่องreturn_valueของmock_openเข้าไปในวัตถุจำลองอื่นและยืนยันที่สองของการเยาะเย้ยreturn_value) แต่มันทำงานโดยการเพิ่มเป็นmock_open new_callable
imrek

1
@ArthurZopellaro ดูที่sixโมดูลเพื่อให้มีmockโมดูลที่สอดคล้องกัน แต่ฉันไม่รู้ว่ามันแมปbuiltinsในโมดูลทั่วไปด้วยหรือไม่
Michele d'Amico

1
คุณจะหาชื่อที่ถูกต้องในการแก้ไขได้อย่างไร? คือคุณจะหาข้อโต้แย้งแรกที่ @patch ('builtins.open' ในกรณีนี้) เพื่อฟังก์ชั่นโดยพลการได้อย่างไร
zenperttu

73

ด้วย mock เวอร์ชันล่าสุดคุณสามารถใช้เครื่องมือช่วยmock_open ที่มีประโยชน์จริงๆ:

mock_open (mock = ไม่มี, read_data = ไม่มี)

ฟังก์ชั่นตัวช่วยในการสร้างจำลองเพื่อแทนที่การใช้งานแบบเปิด มันทำงานสำหรับเปิดเรียกโดยตรงหรือใช้เป็นผู้จัดการบริบท

อาร์กิวเมนต์จำลองเป็นวัตถุจำลองเพื่อกำหนดค่า หากไม่มี (ค่าเริ่มต้น) จะมีการสร้าง MagicMock ให้กับคุณโดย API จะ จำกัด วิธีหรือคุณลักษณะที่มีอยู่ในการจัดการไฟล์มาตรฐาน

read_data เป็นสตริงสำหรับวิธีการอ่านของการจัดการไฟล์ที่จะกลับมา นี่คือสตริงว่างเปล่าโดยค่าเริ่มต้น

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

คุณจะตรวจสอบว่ามีการ.writeโทรหลายสายได้อย่างไร?
n611x007

1
@naxa วิธีหนึ่งคือการผ่านแต่ละพารามิเตอร์ที่คาดhandle.write.assert_any_call()ไว้ คุณยังสามารถใช้handle.write.call_args_listเพื่อรับสายแต่ละครั้งหากการสั่งซื้อมีความสำคัญ
Rob Cutmore

m.return_value.write.assert_called_once_with('some stuff')ดีกว่า IMO หลีกเลี่ยงการลงทะเบียนสาย
ไม่เปิดเผยตัว

2
การยืนยันด้วยตนเองMock.call_args_listนั้นปลอดภัยกว่าการเรียกMock.assert_xxxวิธีการใด ๆ หากคุณสะกดคำผิดใด ๆ หลังซึ่งเป็นคุณลักษณะของเยาะเย้ยพวกเขาจะผ่านไปอย่างเงียบ ๆ
Jonathan Hartley

12

ในการใช้mock_openสำหรับไฟล์อย่างง่ายread()(ตัวอย่าง mock_open ดั้งเดิมที่กำหนดไว้แล้วในหน้านี้เหมาะสำหรับการเขียนมากขึ้น):

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

หมายเหตุตามเอกสารสำหรับ mock_open สิ่งนี้มีไว้เฉพาะread()ดังนั้นจะไม่ทำงานกับรูปแบบทั่วไปfor line in fเช่น

ใช้ python 2.6.6 / จำลอง 1.0.1


ดูดี แต่ฉันไม่สามารถใช้กับfor line in opened_file:ประเภทของรหัสได้ ฉันพยายามทดลองด้วย StringIO ที่ใช้การได้ซึ่งนำไปปฏิบัติ__iter__และใช้สิ่งนั้นแทนmy_textแต่ไม่มีโชค
Evgen

@EvgeniiPuchkaryov สิ่งนี้ใช้ได้เฉพาะread()เพื่อไม่ให้ทำงานในfor line in opened_fileกรณีของคุณ ฉันได้แก้ไขโพสต์เพื่อชี้แจง
jlb83

1
@EvgeniiPuchkaryov for line in f:สนับสนุนสามารถทำได้โดยการเยาะเย้ยค่าตอบแทนของopen()เป็นวัตถุ StringIO แทน
Iskar Jarak

1
หากต้องการชี้แจงระบบภายใต้การทดสอบ (SUT) ในตัวอย่างนี้คือ: with open("any_string") as f: print f.read()
Brad M

4

คำตอบยอดนิยมมีประโยชน์ แต่ฉันขยายมันเล็กน้อย

หากคุณต้องการตั้งค่าของวัตถุไฟล์ ( fในas f) ตามอาร์กิวเมนต์ที่ส่งไปopen()ที่นี่เป็นวิธีหนึ่งในการดำเนินการ:

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

โดยทั่วไปแล้วopen()จะส่งคืนวัตถุและwithจะเรียก__enter__()วัตถุนั้น

ในการเยาะเย้ยอย่างถูกต้องเราต้องเยาะเย้ยopen()เพื่อกลับวัตถุจำลอง วัตถุจำลองนั้นควรเยาะเย้ยการ__enter__()เรียกใช้มัน ( MagicMockจะทำสิ่งนี้ให้เรา) เพื่อคืนค่าวัตถุข้อมูล / ไฟล์จำลองที่เราต้องการ (ด้วยเหตุนี้mm.__enter__.return_value) การทำเช่นนี้ด้วย 2 mocks ตามวิธีด้านบนช่วยให้เราสามารถจับข้อโต้แย้งที่ส่งผ่านopen()และส่งผ่านไปยังdo_something_with_dataวิธีการของเรา

ฉันส่งไฟล์จำลองทั้งหมดเป็นสตริงไปopen()และdo_something_with_dataดูเหมือนว่า:

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

สิ่งนี้จะแปลงสตริงเป็นรายการเพื่อให้คุณสามารถทำสิ่งต่อไปนี้ได้เช่นเดียวกับไฟล์ปกติ:

for line in file:
    #do action

หากโค้ดที่กำลังทดสอบจัดการไฟล์ในวิธีที่ต่างกันตัวอย่างเช่นโดยการเรียกฟังก์ชัน "readline" คุณสามารถส่งคืนวัตถุจำลองที่คุณต้องการในฟังก์ชัน "do_something_with_data" ด้วยฟังก์ชันที่ต้องการ
user3289695

มีวิธีหลีกเลี่ยงการสัมผัส__enter__หรือไม่? แน่นอนว่ามันดูเหมือนแฮ็คมากกว่าวิธีที่แนะนำ
imrek

enterเป็นวิธีที่ผู้จัดการคอนเท็กซ์เช่น open () เขียน Mocks มักจะเป็นบิตแฮ็คที่คุณจำเป็นต้องเข้าถึงสิ่งที่ "ส่วนตัว" เพื่อเยาะเย้ย แต่ป้อนที่นี่ไม่ได้แฮ็ค imo
theannouncer

3

ฉันอาจจะสายไปหน่อยสำหรับเกม แต่สิ่งนี้ใช้ได้กับฉันเมื่อโทรopenในโมดูลอื่นโดยไม่ต้องสร้างไฟล์ใหม่

test.py

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

โดยการปรับปรุงopenฟังก์ชั่นภายใน__builtin__โมดูลกับของmock_open()ฉันฉันสามารถเยาะเย้ยการเขียนไฟล์โดยไม่ต้องสร้าง

หมายเหตุ: หากคุณกำลังใช้โมดูลที่ใช้ cython หรือโปรแกรมของคุณขึ้นอยู่กับ cython ในทางใดทางหนึ่งคุณจะต้องนำเข้าโมดูลcython__builtin__โดยรวมimport __builtin__ที่ด้านบนของไฟล์ของคุณ คุณจะไม่สามารถเยาะเย้ยถ้วนทั่ว__builtin__หากคุณใช้ cython


ความแตกต่างของวิธีการนี้ได้ผลสำหรับฉันเนื่องจากรหัสส่วนใหญ่ภายใต้การทดสอบอยู่ในโมดูลอื่น ๆ ดังที่แสดงไว้ที่นี่ ฉันต้องแน่ใจว่าจะเพิ่มลงimport __builtin__ในโมดูลทดสอบของฉัน บทความนี้ช่วยชี้แจงว่าทำไมเทคนิคนี้ถึงใช้ได้เช่นเดียวกับที่ทำ: ichimonji10.name/blog/6
killthrush

0

ในการแก้ไขฟังก์ชั่น open () ในตัวด้วย unittest:

สิ่งนี้ทำงานได้สำหรับแพตช์เพื่ออ่านการกำหนดค่า json

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

วัตถุที่เย้ยหยันคือวัตถุ io.TextIOWrapper ที่ส่งคืนโดยฟังก์ชัน 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

หากคุณไม่ต้องการไฟล์ใด ๆ เพิ่มเติมคุณสามารถตกแต่งวิธีทดสอบได้:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.