akiyoko blog

akiyoko の IT技術系ブログです

Python で MagicMock を使う

MagicMock は mock.Mock のサブクラスで、薄いラッパーです。

>>> from mock import Mock, MagicMock
>>> issubclass(MagicMock, Mock)
True
>>> list(set(dir(MagicMock)) - set(dir(Mock)))
['_mock_set_magics']



MagicMock を使えば、クラスだろうがオブジェクトだろうがメソッドだろうが関数だろうが、何でもモックオブジェクトで置き換えて、その振る舞いを自由にシュミレートすることができます。



まず、一番シンプルにモックを使う方法は、直接 MagicMock オブジェクトを代入するやり方です。


models.py(置換対象クラス)

import random

class User(object):
    def __init__(self, name, gender='m'):
        self.name = name
        self.gender = gender

    def get_name(self):
        return self.name

    def get_gender(self):
        return self.gender

    def vote(self, *seq):
        return random.choice(seq)

    @classmethod
    def class_method(cls):
        return 'class method'

    @staticmethod
    def static_method():
        return 'static method'


使い方(get_nameメソッドに MagicMockオブジェクトを直接代入)

>>> from mock import MagicMock
>>> import models
>>> user = models.User('akiyoko')
>>> user.get_name = MagicMock()
>>> user.get_name.return_value = 'mock'
>>> user.get_name()
'mock'

代入してモック化する部分は、以下のようにまとめて書くこともできます。

>>> user.get_name = MagicMock(return_value='mock')

あるいは、以下のように get_nameメソッドをほかの関数で置き換えることもできます。

>>> def mock_get_name():
...     return 'mock'
...
>>> user.get_name = MagicMock()
>>> user.get_name.side_effect = mock_get_name


なお、クラスをモックオブジェクトで置き換えて、メソッドの振る舞いを規定する場合には少し注意(慣れ?)が必要です。

>>> from mock import MagicMock
>>> import models
>>> models.User = MagicMock()
>>> models.User.return_value.get_name.return_value = 'mock'
>>> user = models.User('akiyoko')
>>> user.get_name()
'mock'

という感じで、「return_value」を使って、Userクラスと get_nameメソッドを紐付けてやらないといけません。



このように直接代入する使い方はシンプルなのですが、置換対象を恒久的に置き換えてしまうので、(テストコードで使用する場合など)通常は、with句やデコレータでモックを適用する範囲を限定して 使います。


with句やデコレータでは、「patch」あるいは「patch.object」を使って、置換対象のクラスやメソッドを置き換えます。


置き換えることができる対象は、次の表の通りです。

直接代入 patch patch.object
モジュール × × ×
クラス ×
オブジェクト × ×
メソッド
関数
属性 *1
インスタンスオブジェクト *2 ×


 
前置きはここまでにして、今回は、実際のテストコードでよく使う利用シーンごとに使用方法を説明していきたいと思います。


なお、with句で patch (or patch.object) を使う場合とデコレータで patch (or patch.object) を使う場合では、モック化したオブジェクトの取り扱い方やモックの適用範囲が異なってきますが、利用シーンは変わらないのでまとめて説明しています。


 

事前準備

mock パッケージを pip install しておきます。

$ sudo pip install mock

 

確認環境


 

1.クラスを置き換える

【例 1-1】 patch(target) as ... で後から振る舞いを規定する

patch でモック化したオブジェクトは as で(デコレータを使った場合はメソッドの引数として)受け取ることができるので、受け取ったモックオブジェクトに対して後から振る舞いを規定することができます。


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトで置き換える
    with patch('models.User') as mock_user:
        assert isinstance(mock_user, MagicMock)
        # 後でモックオブジェクトの振る舞いを規定する
        mock_user.return_value.get_name.return_value = 'mock'

        user = models.User('akiyoko')
        assert user.get_name() == 'mock'
        assert isinstance(user.get_gender(), MagicMock)


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトで置き換える
@patch('models.User')
def test_user(mock_user):
    assert isinstance(mock_user, MagicMock)
    # 後でモックオブジェクトの振る舞いを規定する
    mock_user.return_value.get_name.return_value = 'mock'

    user = models.User('akiyoko')
    assert user.get_name() == 'mock'
    assert isinstance(user.get_gender(), MagicMock)

(参考)




 
ここで大きなハマりポイントが。

patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)

の第一引数(target)には、

  • import できる形の文字列であること
  • クラス名、メソッド名、関数名のいずれかであること(モジュール名は不可)

といったルールが存在します。


まず、
http://www.voidspace.org.uk/python/mock/patch.html#patch
によると、の第一引数の target は import がされるので、きちんと import できる形で書かないとダメということです。

target should be a string in the form ‘package.module.ClassName’. The target is imported and the specified object replaced with the new object, so the target must be importable from the environment you are calling patch from. The target is imported when the decorated function is executed, not at decoration time.

 
なので、以下のように import できない形で書くとエラーになってしまいます。

with patch('User') as mock_user:
$ nosetests -s
E
======================================================================
ERROR: test_models.test_user
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/akiyoko/github/python-mock/tests/test_models.py", line 8, in test_user
    with patch('User') as mock_user:
  File "/Library/Python/2.7/site-packages/mock.py", line 1564, in patch
    getter, attribute = _get_target(target)
  File "/Library/Python/2.7/site-packages/mock.py", line 1413, in _get_target
    (target,))
TypeError: Need a valid target to patch. You supplied: 'User'

----------------------------------------------------------------------
Ran 1 test in 0.156s

FAILED (errors=1)


 
次に、target が import できる形であっても、import する形の違いによって、patch で置換したはずのクラスが別物になってしまうケースがあります。

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

from models import User

def test_user():
    # モックオブジェクトで置き換える
    with patch('models.User') as mock_user:
        assert isinstance(mock_user, MagicMock)
        # 後でモックオブジェクトの振る舞いを規定する
        mock_user.return_value.get_name.return_value = 'mock'

        user = User('akiyoko')
        assert user.get_name() == 'mock'
        assert isinstance(user.get_gender(), MagicMock)
$ nosetests -s
F
======================================================================
FAIL: test_models.test_user
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/akiyoko/github/python-mock/tests/test_models.py", line 14, in test_user
    assert user.get_name() == 'mock'
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.067s

FAILED (failures=1)


 
少しややこしいですが、この場合の target は以下のように書くのが正解です。

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

from models import User

def test_user():
    # モックオブジェクトで置き換える
    with patch('test_models.User') as mock_user:
        assert isinstance(mock_user, MagicMock)
        # 後でモックオブジェクトの振る舞いを規定する
        mock_user.return_value.get_name.return_value = 'mock'

        user = User('akiyoko')
        assert user.get_name() == 'mock'
        assert isinstance(user.get_gender(), MagicMock)


詳細については、次の記事に詳しく書かれています。


 
このように、patch を使ったときに target に指定する文字列は、取り扱いがややこしいので要注意です。







 

2.メソッドを置き換える

【例 2-1】 patch(target, return_value=...) を使う

return_value で、メソッドが実行されたときの戻り値を規定することができます。

例えば、直接代入する場合には、

>>> from mock import MagicMock
>>> import models
>>> models.User.get_name = MagicMock(return_value='mock')
>>> user = models.User('akiyoko')
>>> user.get_name()
'mock'

と書きます。

メソッド以外にも、classmethod や staticmethod についても同じようにして戻り値を置換することができます。


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # 直接振る舞いを置き換える
    with patch('models.User.get_name', return_value='mock') as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ patch.object を使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # 直接振る舞いを置き換える
    with patch.object(models.User, 'get_name', return_value='mock') as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# 直接振る舞いを置き換える
@patch('models.User.get_name', return_value='mock')
def test_user(_mock_get_name):
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert _mock_get_name.call_count == 1


tests/test_models.py(※ patch.object + デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# 直接振る舞いを置き換える
@patch.object(models.User, 'get_name', return_value='mock')
def test_user(_mock_get_name):
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert _mock_get_name.call_count == 1


 

【例 2-2】 patch(target, side_effect=...) を使う

side_effect で、メソッドの定義そのものを置き換える関数を指定することができます。


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_get_name():
    return 'mock'

def test_user():
    # 直接振る舞いを置き換える
    with patch('models.User.get_name', side_effect=mock_get_name) as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ patch.object を使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_get_name():
    return 'mock'

def test_user():
    # 直接振る舞いを置き換える
    with patch.object(models.User, 'get_name', side_effect=mock_get_name) as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_get_name():
    return 'mock'

# 直接振る舞いを置き換える
@patch('models.User.get_name', side_effect=mock_get_name)
def test_user(_mock_get_name):
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert _mock_get_name.call_count == 1


tests/test_models.py(※ patch.object + デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_get_name():
    return 'mock'

# 直接振る舞いを置き換える
@patch.object(models.User, 'get_name', side_effect=mock_get_name)
def test_user(_mock_get_name):
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert _mock_get_name.call_count == 1


 

【例 2-3】 patch(target, new) を使う

patch の第2引数、および patch.object の第3引数に、置き換えるモックオブジェクトを指定することもできます。ただし、指定できるのはモックオブジェクトに限ります。


(参考)

patch.object の呼び出しには3引数の形式と2引数の形式があります。 3引数の場合、 patch 対象のオブジェクト、属性名、その属性を置き換えるオブジェクトを取ります。

2引数の形式では、置き換えるオブジェクトを省略し、生成された mock がデコレート対象となる関数に追加の引数として渡されます。


http://docs.python.jp/3/library/unittest.mock.html#patch-object


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトの振る舞いを規定する
    mock_get_name = MagicMock(return_value='mock')

    # モックオブジェクトで置き換える
    with patch('models.User.get_name', mock_get_name) as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1

↓ は、↑ と同義です。

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトで置き換える
    with patch('models.User.get_name', MagicMock(return_value='mock')) as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ patch.object を使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトで置き換える
    with patch.object(models.User, 'get_name', MagicMock(return_value='mock')) as _mock_get_name:
        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert _mock_get_name.call_count == 1


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトで置き換える
@patch('models.User.get_name', MagicMock(return_value='mock'))
def test_user():
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'


tests/test_models.py(※ patch.object + デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトで置き換える
@patch.object(models.User, 'get_name', MagicMock(return_value='mock'))
def test_user():
    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'


 

【例 2-4】 patch(target) as ... で後から振る舞いを規定する

patch でモック化したオブジェクトは as で(デコレータを使った場合はメソッドの引数として)受け取ることができるので、受け取ったモックオブジェクトに対して後から振る舞いを規定することができます。


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトで置き換える
    with patch('models.User.get_name') as mock_get_name:
        assert isinstance(mock_get_name, MagicMock)
        # 後でモックオブジェクトの振る舞いを規定する
        mock_get_name.return_value = 'mock'

        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert mock_get_name.call_count == 1


tests/test_models.py(※ patch.object を使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトで置き換える
    with patch.object(models.User, 'get_name') as mock_get_name:
        assert isinstance(mock_get_name, MagicMock)
        # 後でモックオブジェクトの振る舞いを規定する
        mock_get_name.return_value = 'mock'

        user = models.User('akiyoko')
        assert user.__class__.__name__ == 'User'
        assert user.get_name() == 'mock'
        assert user.get_gender() == 'm'
        assert mock_get_name.call_count == 1


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトで置き換える
@patch('models.User.get_name')
def test_user(mock_get_name):
    assert isinstance(mock_get_name, MagicMock)
    # 後でモックオブジェクトの振る舞いを規定する
    mock_get_name.return_value = 'mock'

    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert mock_get_name.call_count == 1


tests/test_models.py(※ patch.object + デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトで置き換える
@patch.object(models.User, 'get_name')
def test_user(mock_get_name):
    assert isinstance(mock_get_name, MagicMock)
    # 後でモックオブジェクトの振る舞いを規定する
    mock_get_name.return_value = 'mock'

    user = models.User('akiyoko')
    assert user.__class__.__name__ == 'User'
    assert user.get_name() == 'mock'
    assert user.get_gender() == 'm'
    assert mock_get_name.call_count == 1




 

3.関数を置き換える

メソッドを置き換えるケースとほぼ同じなので、わざわざ分けて説明する必要もないのですが、一応。(なるべく、これまでと少し違う系統のサンプルコードを載せてみます。)

 

【例 3-1】 patch(target, return_value=...) を使う

tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # 直接振る舞いを置き換える
    with patch('random.choice', return_value=0) as _mock_choice:
        user = models.User('akiyoko')
        assert user.vote(0, 1, 2) == 0
        assert _mock_choice.call_count == 1


 

【例 3-2】 patch(target, side_effect=...) を使う

tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_choice(seq):
    return seq[0]

def test_user():
    # 直接振る舞いを置き換える
    with patch('random.choice', side_effect=mock_choice) as _mock_choice:
        user = models.User('akiyoko')
        assert user.vote(*range(1000)) == 0
        assert _mock_choice.call_count == 1


side_effect は、任意の例外を発生させるケースでも利用されます。

tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def mock_choice(seq):
    return seq[0]

def test_user():
    # 直接振る舞いを置き換える
    with patch('random.choice', side_effect=TypeError('test')) as _mock_choice:
        user = models.User('akiyoko')
        try:
            user.vote(0, 1, 2)
        except TypeError:
            assert True
        except:
            assert False, "Unexpected exception thrown"
        else:
            assert False, "Exception not thrown"


 

【例 3-3】 patch(target, new) を使う

このような使い方も。

>>> from mock import MagicMock, patch
>>> import datetime
>>> target = datetime.datetime(2009, 1, 1)
>>> with patch.object(datetime, 'datetime', MagicMock(wraps=datetime.datetime)) as patched:
...     patched.now.return_value = target
...     print(datetime.datetime.now())
...
2009-01-01 00:00:00

Patching datetime.date.today() across Python implementations


 

【例 3-4】 patch(target) as ... で後から振る舞いを規定する

こういう使い方でビルトイン関数をモック化することができます。

import mock

def foo():
    for line in open('myfile'):
        print line

@mock.patch('__builtin__.open')
def test_foo(open_mock):
    foo()
    assert open_mock.called

tdd - Mocking file objects or iterables in python - Stack Overflow



こういった応用例もあります。

>>> from StringIO import StringIO
>>> def foo():
...     print 'Something'
...
>>> @patch('sys.stdout', new_callable=StringIO)
... def test(mock_stdout):
...     foo()
...     assert mock_stdout.getvalue() == 'Something\n'
...
>>> test()

http://www.voidspace.org.uk/python/mock/patch.html#patch







 

4.属性を置き換える

省略。
あまり使わないですもんね。。






 

5. (クラスがインスタンス化された際の)インスタンスオブジェクトを置き換える

最後はこれまでとは少し異なったパターンかもしれません。

 

【例 5-1】 patch(クラス名, return_value=...) を使う

patch(クラス名, return_value=...) を使うと、クラスがインスタンス化された際のインスタンスオブジェクトを置き換えることができます。


tests/test_models.py

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

def test_user():
    # モックオブジェクトの振る舞いを規定する
    mock_user = MagicMock()
    mock_user.get_name.return_value = 'mock'

    # モックオブジェクトで置き換える
    with patch('models.User', return_value=mock_user) as _mock_user:
        user = models.User('akiyoko')
        assert isinstance(user, MagicMock)
        assert user.get_name() == 'mock'
        assert isinstance(user.get_gender(), MagicMock)
        assert _mock_user.call_count == 1


tests/test_models.py(※ デコレータを使って書き換えたもの)

# -*- coding: utf-8 -*-
from mock import MagicMock, patch

import models

# モックオブジェクトの振る舞いを規定する
mock_user = MagicMock()
mock_user.get_name.return_value = 'mock'

# モックオブジェクトで置き換える
@patch('models.User', return_value=mock_user)
def test_user(_mock_user):
    user = models.User('akiyoko')
    assert isinstance(user, MagicMock)
    assert user.get_name() == 'mock'
    assert isinstance(user.get_gender(), MagicMock)
    assert _mock_user.call_count == 1




 

*1: mock.PropertyMock を使います

*2: models.User.__new__ = MagicMock(return_value='mock') という形で代入すればモック化可能