Bagaimana cara mengejek open yang digunakan dalam pernyataan with (menggunakan framework Mock dengan Python)?

188

Bagaimana cara menguji kode berikut dengan mengolok-olok (menggunakan mengolok-olok, dekorator patch dan penjaga disediakan oleh kerangka kerja Mock Michael Foord ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()
Daryl Spitzer
sumber
@Daryl Spitzer: bisakah Anda meninggalkan pertanyaan-meta ("Saya tahu jawabannya ...") Itu membingungkan.
S.Lott
Di masa lalu ketika saya tinggalkan, orang-orang mengeluh bahwa saya menjawab pertanyaan saya sendiri. Saya akan mencoba memindahkan itu ke jawaban saya.
Daryl Spitzer
1
@Daryl: Cara terbaik untuk menghindari keluhan tentang menjawab pertanyaan sendiri, yang biasanya berasal dari kekhawatiran "pelacur karma", adalah dengan menandai pertanyaan dan / atau menjawab sebagai "wiki komunitas".
John Millikin
3
Jika menjawab pertanyaan Anda sendiri dianggap Karma Whoring, saya kira FAQ harus diklarifikasi tentang hal itu.
EBGreen

Jawaban:

132

Cara untuk melakukan ini telah berubah di mock 0.7.0 yang akhirnya mendukung mengejek metode protokol python (metode ajaib), terutama menggunakan MagicMock:

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

Contoh mengejek terbuka sebagai manajer konteks (dari halaman contoh dalam dokumentasi tiruan):

>>> 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')
fuzzyman
sumber
Wow! Ini terlihat jauh lebih sederhana daripada contoh konteks-manajer saat ini di voidspace.org.uk/python/mock/magicmock.html yang secara eksplisit menetapkan __enter__dan __exit__untuk mengolok-olok objek juga - apakah pendekatan yang terakhir sudah ketinggalan zaman, atau masih berguna?
Brandon Rhodes
6
"Pendekatan terakhir" menunjukkan bagaimana melakukannya tanpa menggunakan MagicMock (yaitu hanya contoh bagaimana Mock mendukung metode sihir). Jika Anda menggunakan MagicMock (seperti di atas) maka masuk dan keluar sudah dikonfigurasikan untuk Anda.
fuzzyman
5
Anda dapat menunjuk ke posting blog Anda di mana Anda menjelaskan lebih detail mengapa / bagaimana cara kerjanya
Rodrigue
9
Dalam Python 3, 'file' tidak didefinisikan (digunakan dalam MagicMock spec), jadi saya menggunakan io.IOBase sebagai gantinya.
Jonathan Hartley
1
Catatan: dalam Python3 builtin filehilang!
exhuma
239

mock_openadalah bagian dari mockframework dan sangat mudah digunakan. patchdigunakan sebagai konteks mengembalikan objek yang digunakan untuk menggantikan yang ditambal: Anda dapat menggunakannya untuk membuat tes Anda lebih sederhana.

Python 3.x

Gunakan builtinssebagai ganti __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

mockbukan bagian dari unittestdan Anda harus menambal__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")

Kasus dekorator

Jika Anda akan menggunakan patchsebagai dekorator menggunakan mock_open()hasil sebagai new patchargumen bisa sedikit aneh.

Dalam hal ini lebih baik menggunakan new_callable patchargumen dan ingat bahwa setiap argumen tambahan yang patchtidak digunakan akan diteruskan ke new_callablefungsi seperti yang dijelaskan dalam patchdokumentasi .

patch () mengambil argumen kata kunci sewenang-wenang. Ini akan diteruskan ke Mock (atau new_callable) pada konstruksi.

Misalnya versi yang didekorasi untuk Python 3.x adalah:

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

Ingatlah bahwa dalam hal ini patchakan menambahkan objek tiruan sebagai argumen fungsi pengujian Anda.

Michele d'Amico
sumber
Maaf karena ditanya, dapatkah with patch("builtins.open", mock_open(read_data="data")) as mock_file:dikonversi menjadi sintaksis dekorator? Saya sudah mencoba, tetapi saya tidak yakin apa yang perlu saya sampaikan @patch("builtins.open", ...) sebagai argumen kedua.
imrek
1
@DrunkenMaster Updateted .. terima kasih sudah menunjukkannya. Menggunakan dekorator tidak sepele dalam hal ini.
Michele d'Amico
Grazie! Masalah saya adalah sedikit lebih kompleks (saya harus menyalurkan return_valuedari mock_openke objek tiruan lain dan menegaskan mock kedua ini return_value), tapi itu bekerja dengan menambahkan mock_opensebagai new_callable.
imrek
1
@ArthurZopellaro melihat ke sixmodul untuk memiliki mockmodul yang konsisten . Tapi saya tidak tahu apakah itu memetakan juga builtinsdalam modul umum.
Michele d'Amico
1
Bagaimana Anda menemukan nama yang benar untuk ditambal? Yaitu bagaimana Anda menemukan argumen pertama ke @patch ('builtins.open' dalam kasus ini) untuk fungsi sewenang-wenang?
zenperttu
73

Dengan versi mock terbaru, Anda dapat menggunakan pembantu mock_open yang sangat berguna :

mock_open (mock = Tidak Ada, read_data = Tidak Ada)

Fungsi pembantu untuk membuat tiruan untuk menggantikan penggunaan terbuka. Ini berfungsi untuk terbuka yang dipanggil langsung atau digunakan sebagai manajer konteks.

Argumen mock adalah objek tiruan untuk dikonfigurasi. Jika Tidak Ada (default) maka MagicMock akan dibuat untuk Anda, dengan API terbatas pada metode atau atribut yang tersedia pada pegangan file standar.

read_data adalah string untuk metode baca dari pegangan file untuk kembali. Ini adalah string kosong secara default.

>>> 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')
David
sumber
bagaimana Anda memeriksa apakah ada beberapa .writepanggilan?
n611x007
1
@naxa Salah satu caranya adalah meneruskan setiap parameter yang diharapkan handle.write.assert_any_call(). Anda juga dapat menggunakan handle.write.call_args_listuntuk mendapatkan setiap panggilan jika urutannya penting.
Rob Cutmore
m.return_value.write.assert_called_once_with('some stuff')lebih baik imo. Hindari mendaftarkan panggilan.
Anonim
2
Menegaskan tentang secara manual Mock.call_args_listlebih aman daripada memanggil Mock.assert_xxxmetode apa pun. Jika Anda salah mengeja salah satu dari yang terakhir, menjadi atribut Mock, mereka akan selalu diam-diam berlalu.
Jonathan Hartley
12

Untuk menggunakan mock_open untuk file sederhana read()(snipet mock_open asli yang sudah diberikan pada halaman ini lebih diarahkan untuk menulis):

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

Catatan sesuai dokumen untuk mock_open, ini khusus untuk read(), jadi tidak akan berfungsi dengan pola umum sepertifor line in f , misalnya.

Menggunakan python 2.6.6 / mock 1.0.1

jlb83
sumber
Terlihat bagus, tapi saya tidak bisa membuatnya bekerja dengan for line in opened_file:jenis kode. Saya mencoba bereksperimen dengan StringIO yang dapat diterapkan yang mengimplementasikan __iter__dan menggunakannya bukan my_text, tetapi tidak berhasil.
Evgen
@EvgeniiPuchkaryov Ini berfungsi khusus untuk read()jadi tidak akan berfungsi dalam for line in opened_filekasus Anda ; Saya telah mengedit posting untuk memperjelas
jlb83
1
for line in f:Dukungan @EvgeniiPuchkaryov dapat dicapai dengan mengejek nilai pengembalian open()sebagai objek StringIO .
Iskar Jarak
1
Untuk memperjelas, sistem yang diuji (SUT) dalam contoh ini adalah: with open("any_string") as f: print f.read()
Brad M
4

Jawaban teratas berguna tetapi saya sedikit memperluasnya.

Jika Anda ingin menetapkan nilai objek file Anda ( fdalam as f) berdasarkan argumen yang diteruskan ke open()berikut adalah salah satu cara untuk melakukannya:

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

Pada dasarnya, open()akan mengembalikan suatu objek dan withakan memanggil __enter__()objek itu.

Untuk mengejek dengan benar, kita harus mengejek open()untuk mengembalikan objek tiruan. Objek tiruan itu kemudian harus mengejek __enter__()panggilan di atasnya ( MagicMockakan melakukan ini untuk kita) untuk mengembalikan objek data / file tiruan yang kita inginkan (karenanya mm.__enter__.return_value). Melakukan ini dengan 2 mengejek cara di atas memungkinkan kita untuk menangkap argumen yang diteruskan ke open()dan meneruskannya ke do_something_with_datametode kita .

Saya melewati seluruh file tiruan sebagai string open()dan saya do_something_with_datatampak seperti ini:

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

Ini mengubah string menjadi daftar sehingga Anda dapat melakukan hal berikut seperti yang Anda lakukan dengan file normal:

for line in file:
    #do action
theannouncer
sumber
Jika kode yang diuji menangani file dengan cara yang berbeda, misalnya dengan memanggil fungsinya "readline", Anda dapat mengembalikan objek tiruan yang Anda inginkan dalam fungsi "do_something_with_data" dengan atribut yang diinginkan.
user3289695
Apakah ada cara untuk menghindari sentuhan __enter__? Ini jelas lebih mirip hack daripada cara yang disarankan.
imrek
enter adalah bagaimana manajer conext seperti open () ditulis. Mock akan sering menjadi sedikit macet karena Anda perlu mengakses barang-barang "pribadi" untuk diejek, tetapi masuk di sini bukan imo yang tidak terputus
theannouncer
3

Saya mungkin agak terlambat ke permainan, tetapi ini berhasil bagi saya ketika memanggil openmodul lain tanpa harus membuat file baru.

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

Dengan menambal openfungsi di dalam __builtin__modul kemock_open() , saya bisa mengejek menulis ke file tanpa membuat satu.

Catatan: Jika Anda menggunakan modul yang menggunakan cython, atau program Anda bergantung pada cython, Anda perlu mengimpor modul cython__builtin__ dengan memasukkan import __builtin__di bagian atas file Anda. Anda tidak akan dapat mengejek universal __builtin__jika Anda menggunakan cython.

Leo C Han
sumber
Variasi dari pendekatan ini berhasil bagi saya, karena sebagian besar kode yang diuji ada di modul lain seperti yang ditunjukkan di sini. Saya memang perlu memastikan untuk menambah import __builtin__modul pengujian saya. Artikel ini membantu memperjelas mengapa teknik ini bekerja sebaik itu: ichimonji10.name/blog/6
killthrush
0

Untuk menambal fungsi buka () bawaan dengan unittest:

Ini berfungsi untuk tambalan untuk membaca konfigurasi json.

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

Objek yang dipermainkan adalah objek io.TextIOWrapper yang dikembalikan oleh fungsi 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):
pabloberm
sumber
0

Jika Anda tidak memerlukan file lebih jauh, Anda dapat menghias metode pengujian:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
Ferdinando de Melo
sumber