Pythonで、MagicMockのreturn_valueを使って、モックから別のモックを返してみた

Pythonにて、「モックから別のモックを返す」テストコードを作成する機会がありました。

そこで、以下を参考に作成した時のメモを残します。

 
目次

 

環境

  • Python 3.6.0
    • unittest.mock.MagicMockを使用
  • pytest 3.0.6
    • テストランナーとして使用

 

状況

Targetクラスのrun()メソッドについて、戻り値の辞書に正しく値が設定されているかを確認します。

target.py

from cook import Cook

class Target:
    def run(self, material):
        # 引数materialは作るのに手間がかかるオブジェクト
        cook = Cook(material)
        cuisine = cook.bake()
        return {
            'name': 'maindish',
            'cuisine': cuisine.get_name(),
        }

 
Cookクラスのbake()メソッドでは、さらに別のクラス(Ham, Spam, Egg)のインスタンスを返します。

cook.py

from cuisine import Ham, Spam, Egg

class Cook:
    def __init__(self, material):
        self.__material = material

    def bake(self):
        try:
            # 実際には、materialの中身によって複雑な処理がある
            if material:
                return Ham()
            else:
                return Spam()
        except:
            # なかなかEggを出すデータを作れないとする
            return Egg()

 
Ham, Spam, Eggの各クラスは、それぞれget_name()メソッドを持っています。

cuisine.py

class Ham:
    def get_name(self):
        return 'ham'

class Spam:
    def get_name(self):
        return 'spam'

class Egg:
    def get_name(self):
        return 'egg'

 
そんな中で、Target.run()の戻り値を検証しようとしましたが、

  • get_name()で取得するオブジェクトは、run()の引数materialに依存する
  • 引数material用のオブジェクトを生成するのは難しい

と、このままではテスト作成に手間がかかりそうでした。

 

対応

そこで今回は、unittest.mock.MagicMockを使って2つモックを作ることで、引数material用のオブジェクトを生成しなくても済むようにします*1

 
1つは、戻り値としてeggを返すget_name()メソッドを持つモックです。

<MagicMockオブジェクト>.<差し替えたいメソッド名>.return_valueに、戻り値eggをセットします。

from unittest.mock import MagicMock

def test_bake():
    # cuisineのモックを作る
    mock_cuisine = MagicMock()
    # get_name()メソッドは、'egg'を返すように指定
    mock_cuisine.get_name.return_value = 'egg'

 
もう1つは、bake()メソッドを持ち、上記で作成したモックmock_cuisineを返すモックです。このモックはCookクラスと差し替えるのに使います。

import cook

def test_bake():
    ...
    # bake()メソッドでcuisineのモックを返す、モックを作る
    mock_cook = MagicMock()
    # bake()メソッドは、上記で作ったモック'mock_cuisine'を返すように指定
    mock_cook.bake.return_value = mock_cuisine
    # Cookクラスはモックを返すモックに差し替える
    cook.Cook = mock_cook

 
準備ができたため、あとはテストコードを書きます。

from target import Target

def test_bake():
    ...
    sut = Target()
    # runの引数materialは、本来は作るのが面倒
    # 今回はモック向けなので、何かあれば良い
    actual = sut.run('dummy')
    assert actual['cuisine'] == 'egg'

 
実行してみます。

$ python -m pytest
==== test session starts ====
platform darwin -- Python 3.6.0, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /Users/kamijoshinya/thinkami/try/python_mock_sample, inifile: pytest.ini
collected 1 items 

test_get_mock_object.py .

==== 1 passed in 0.07 seconds ====

Target.run()メソッドの戻り値が差し替わり、テストがpassしました。

 

ソースコード

GitHubにあげました。e.g._get_mock_objectディレクトリの中が今回のファイルです。
thinkAmi-sandbox/python_mock-sample

*1:他の解決方法があるかもしれませんが、今回はモックから別のモックを返すための例なので…