(PythonのMockフレームワークを使用して)withステートメントで使用されているopenをモックするにはどうすればよいですか?


188

モックを使用して次のコードをテストするにはどうすればよいですか(モック、パッチデコレーター、およびMichael Foordのモックフレームワークによって提供されるセンチネルを使用)。

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

@Daryl Spitzer:メタ質問を省いてくれませんか(「私は答えを知っています...」)混乱します。
S.Lott、2009

過去に私がそれをやめたとき、私は自分の質問に答えていると人々は不平を言いました。それを私の答えに移してみます。
ダリルス

1
@Daryl:自分の質問への回答に関する苦情を回避する最良の方法は、通常「カルマ売春」の心配から生じますが、質問や回答を「コミュニティウィキ」としてマークすることです。
ジョンミリキン、

3
自分の質問に答えることがカルマ娼婦であると考えられるなら、私はその点についてFAQを明確にすべきです。
EBGreen、2009

回答:


131

これを行う方法はモック0.7.0で変更され、特にMagicMockを使用して、Pythonプロトコルメソッド(マジックメソッド)のモックを最終的にサポートします。

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

うわー!これは、現在voidspace.org.uk/python/mock/magicmock.htmlにあり、オブジェクトを明示的に設定__enter____exit__てモックするコンテキストマネージャーの例よりもはるかに単純に見えます。後者の方法は時代遅れですか、それともまだ役に立ちますか?
Brandon Rhodes、

5
「後者のアプローチ」は、MagicMock 使用せずにそれを行う方法を示しています(つまり、Mockがマジックメソッドをサポートする方法の単なる例です)。(上記のように)MagicMockを使用する場合、EnterExitは事前に構成されています。
fuzzyman、

5
その理由と仕組みを詳しく説明しているブログ投稿をポイントすることができます
ロドリゲ

9
Python 3では、「ファイル」が定義されていない(MagicMock仕様で使用されている)ため、代わりにio.IOBaseを使用しています。
ジョナサンハートレー

1
注:Python3では、組み込みfileはなくなりました!
exhuma 14

239

mock_openmockフレームワークの一部であり、使用方法は非常に簡単です。patchコンテキストとして使用すると、パッチが適用されたオブジェクトを置き換えるために使用されるオブジェクトが返されます。これを使用して、テストを簡単にすることができます。

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の引数を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", ...) 2番目の引数として何を渡す必要があるのか​​わかりません。
imrek

1
@DrunkenMaster Updateted ..指摘してくれてありがとう。この場合、デコレータの使用は簡単ではありません。
Michele d'Amico

グラツィー!私の問題は、(私がチャンネルにいた少し複雑だったreturn_valueのをmock_open別のモックオブジェクトに第二モックのASSERT return_value)が、それは追加することによって、働いていたmock_openようnew_callable
imrek

1
@ArthurZopellaroは、sixモジュールを調べて、一貫性のあるmockモジュールを作成しています。しかし、それbuiltinsが共通モジュールにもマッピングされているかどうかはわかりません。
Michele d'Amico、

1
パッチする正しい名前をどのようにして見つけますか?つまり、任意の関数に対して@patchの最初の引数(この場合は 'builtins.open')をどのように見つけますか?
zenperttu 2018年

73

mockの最新バージョンでは、本当に便利なmock_openヘルパーを使用できます。

mock_open(mock = None、read_data = None)

openの使用を置き換えるモックを作成するヘルパー関数。直接呼び出された、またはコンテキストマネージャーとして使用されたopenに対して機能します。

モック引数は、構成するモックオブジェクトです。None(デフォルト)の場合、MagicMockが作成され、APIは標準のファイルハンドルで使用できるメソッドまたは属性に制限されます。

read_dataは、返されるファイルハンドルのreadメソッドの文字列です。これはデフォルトでは空の文字列です。

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

1
@naxa 1つの方法は、予想される各パラメーターをに渡すことhandle.write.assert_any_call()です。handle.write.call_args_list順序が重要な場合は、を使用して各呼び出しを取得することもできます。
Rob Cutmore、2015

m.return_value.write.assert_called_once_with('some stuff')イモがいいです。通話の登録を回避します。
2016

2
について手動でアサートする方Mock.call_args_listMock.assert_xxx、メソッドを呼び出すよりも安全です。Mockの属性である後者のスペルを間違えた場合、それらは常に黙ってパスします。
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:、コードの種類を処理できません。の__iter__代わりにそれを実装して使用する反復可能なStringIOを試してみましたmy_textが、うまくいきませんでした。
Evgen、2015年

@EvgeniiPuchkaryovこれは特に機能するread()ため、あなたのfor line in opened_fileケースでは機能しません。私は明確にするために投稿を編集しました
jlb83

1
@EvgeniiPuchkaryovのfor line in f:サポートは、代わりにStringIOオブジェクトopen()としての戻り値をモックすることで実現できます。
Iskar Jarak

1
明確にするため、この例ではテスト(SUT)の下でシステムがある: with open("any_string") as f: print f.read()
ブラッドM

4

上の答えは役に立ちますが、少し拡張しました。

ここに渡された引数に基づいてファイルオブジェクト(fin as f)の値を設定する場合は、次open()の1つの方法があります。

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つのモックでこれを行うと、渡された引数をキャプチャ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 2017

触れ__enter__ないようにする方法はありますか?それは確かに推奨される方法よりもハックのように見えます。
imrek

enterは、open()のようなconextマネージャーがどのように記述されるかです。モックは、モックするために「プライベート」なものにアクセスする必要があるという点で、少しハックになりますが、ここに入力するのは、実際にはハックされていない
imo

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__にmyをパッチするmock_open()ことで、ファイルを作成せずにファイルへの書き込みを模擬できます。

注:用途はcythonというモジュールを使用している、またはあなたのプログラムがどのような方法でcythonに依存している場合は、インポートする必要がありますcythonの__builtin__モジュールを含むことによってimport __builtin__、ファイルの先頭に。__builtin__cythonを使用している場合、ユニバーサルをモックすることはできません。


ここに示すように、テスト中のコードの大部分が他のモジュールにあったため、このアプローチのバリエーションがうまくいきました。import __builtin__テストモジュールに確実に追加する必要がありました。この記事は、この手法がうまく機能する理由を明らかにするのに役立ちました:ichimonji10.name/blog/6
killthrush 2015年

0

組み込みのopen()関数にunittestをパッチするには:

これは、json設定を読み取るパッチで機能しました。

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

モックされたオブジェクトは、open()関数によって返されるio.TextIOWrapperオブジェクトです

@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.