読者です 読者をやめる 読者になる 読者になる

Pythonで、モックに差し替えたメソッドが呼ばれた回数や呼ばれた時の引数を検証する

Python mock テスト

Pythonにて、「モックに差し替えたメソッドが呼ばれた回数や呼ばれた時の引数を検証する」テストコードを作成する機会があったため、メモを残します。

目次

 

環境

  • Python 3.6.0
    • モックは、unittest.mock.MagicMock
  • pytest 3.0.6
    • テストランナーとして使用

 

状況

複雑な処理はするけど戻り値を返さないテスト対象メソッドTarget.target_method()があります。

called_count.py

import called_count_library

class Target(object):
    def target_method(self):
        c = called_count_library.Complex()
        c.set_complex('ham')
        c.set_complex('spam')
        c.set_complex('egg')
        c.set_complex('egg')
        c.set_complex_dict('hoge', {'fuga': 'piyo', 'くだもの': 'りんご'})
        c.set_complex_with_keyword('foo', str_arg='bar', dict_arg={'baz': 'qux', 'quux': 'foobar'} )

 
importして使っているcalled_count_library.pyの各メソッドも、複雑な処理をしている上に戻り値を返さないものでした。

なお、called_count_library.pyは十分にテストされているものとします。

called_count_library.py

class Complex(object):
    def set_complex(self, key):
        # 複雑な処理だけど、戻り値を返さない
        pass

    def set_complex_dict(self, key, dict):
        # 複雑な処理だけど、戻り値を返さない
        pass

    def set_complex_with_keyword(self, no_keyword, str_arg, dict_arg):
        # 複雑な処理だけど、戻り値を返さない
        pass

    def uncall_method(self):
        # 呼ばれないメソッド
        pass

 

対応

called_count.Target.target_method()のテストを書きます。

importしているcalled_count_library.pyは十分にテストされているため、今回は

  • called_count_library.Complexをモックに差し替え
  • モックを使って、メソッド(set_complexなど)が何回呼ばれたかを検証
  • モックを使って、メソッドを呼び出した時の引数がどんなものだったかを検証
  • モックを使って、呼んでいないメソッドを検証

としました。

 
検証方法については、unittest.mock.Mockunittest.mock.MagicMockには検証用のメソッドや属性があるため、それを利用してテストします。

テスト対象のメソッド呼び出しまではこんな感じです。unittest.mock.patch()を使ってComplexクラスを差し替えます。  

from unittest.mock import patch, MagicMock, call
from called_count import Target

class Test_Target(object):
    def test_valid(self):
        mock_lib = MagicMock()
        with patch('called_count_library.Complex', return_value=mock_lib):
            sut = Target()
            sut.target_method()

 

メソッドが呼ばれた回数を検証

<モックオブジェクト>.<検証対象のメソッド>.<検証メソッド>で、対象クラスのメソッドを検証します。

検証メソッド・属性として

  • called
    • 呼ばれたか
  • assert_called()
    • 呼ばれたか
  • assert_called_once()
    • 1回だけ呼ばれたか
  • call_count
    • 呼ばれた回数

があります。

# 複数回呼んでるメソッドの確認
## set_complex()が呼ばれたか
assert mock_lib.set_complex.called is True

## set_complex()が1回でも呼ばれたか
mock_lib.set_complex.assert_called()

## set_complex()を呼んだ回数は4回か
assert mock_lib.set_complex.call_count == 4


# 1回だけ呼んでるメソッドの確認
## set_complex_dict()が1回呼ばれたか
mock_lib.set_complex_dict.assert_called_once()

## set_complex_dict()を呼んだ回数は1回か
assert mock_lib.set_complex_dict.call_count == 1

 

メソッドが呼ばれた時の引数を検証

<モックオブジェクト>.<検証対象のメソッド>.<検証用属性>で、対象クラスのメソッドを検証します。

検証用の属性として

  • call_args
    • 最後に呼ばれた時の引数を取得
  • call_args_list
    • 呼ばれた順に、引数をリストとして取得
    • callオブジェクトがリストになっているため、要素を指定してアンパックすることで、中身をタプルとして得られる

があります。

# call_argsの例
## 引数が1個の場合
args, kwargs = mock_lib.set_complex.call_args
assert args[0] == 'egg'
assert kwargs == {}

## 引数が複数の場合
multi_args, multi_kwargs = mock_lib.set_complex_dict.call_args
assert multi_args[0] == 'hoge'
assert multi_args[1] == {'fuga': 'piyo', 'くだもの': 'りんご'}
assert multi_kwargs == {}

## 名前付き引数を使っている場合
exist_args, exist_kwargs = mock_lib.set_complex_with_keyword.call_args
assert exist_args[0] == 'foo'
assert exist_kwargs == {'str_arg': 'bar', 'dict_arg': {'baz': 'qux', 'quux': 'foobar'}}


# call_args_listの例
list_args = mock_lib.set_complex.call_args_list
assert list_args == [call('ham'), call('spam'), call('egg'), call('egg')]
## callの中身はアンパックで取得
unpack_args, unpack_kwargs = list_args[0]
assert unpack_args == ('ham', )
assert unpack_kwargs == {}

 

メソッドが呼ばれた回数と引数を同時に検証

<モックオブジェクト>.<検証対象のメソッド>.<検証メソッド>で、対象クラスのメソッドを検証します。

検証メソッドとして

  • assert_called_with()
    • 最後に呼ばれた時の引数が一致するか
  • assert_called_once_with()
    • 指定した引数で1回だけ呼ばれたか
  • assert_any_call()
    • 指定した引数の呼び出しがあったか
  • assert_has_calls()
    • 順番通りに呼ばれたか
    • 順番通りでなくてもよいが、どの引数も呼ばれたか (any_order=True)

があります。

# 最後に呼んだ時の引数は'egg'か
mock_lib.set_complex.assert_called_with('egg')
# 引数'ham'は一番最初で呼んでいるため、以下の書き方ではテストが失敗する
# mock_lib.set_complex.assert_called_with('ham')

# 引数'hoge', {'fuga': 'piyo', 'くだもの': 'りんご'}で、1回だけ呼ばれたか
mock_lib.set_complex_dict.assert_called_once_with('hoge', {'fuga': 'piyo', 'くだもの': 'りんご'})

# 引数'ham'や'spam'で呼ばれたか
mock_lib.set_complex.assert_any_call('ham')
mock_lib.set_complex.assert_any_call('spam')

# 引数が'ham' > 'spam' > 'egg' > 'egg' の順で呼ばれたか
mock_lib.set_complex.assert_has_calls([call('ham'), call('spam'), call('egg'), call('egg')])
# ちなみに、同じ引数がある場合、片方を省略してもPASSした
mock_lib.set_complex.assert_has_calls([call('ham'), call('spam'), call('egg')])
# 順番は気にしないけど、どの引数でも呼ばれたか
mock_lib.set_complex.assert_has_calls([call('spam'), call('egg'), call('ham')], any_order=True)

 

メソッドが呼ばれていないことを検証

<モックオブジェクト>.<検証対象のメソッド>.<検証メソッド>で、対象クラスのメソッドを検証します。

検証メソッドはassert_not_calledです。

# uncall()は1回も呼ばれていないか
mock_lib.uncall_method.assert_not_called()

 

参考

 

ソースコード

GitHubに上げました。e.g._called_countディレクトリが今回のものです。
thinkAmi-sandbox/python_mock-sample