【python】mockの使い方まとめ

python

こんにちは、コウキです。

mockとは、関数やクラスの機能を置き換えるもののことです。
テストコードを書くときによく使います。

テストのときだけ、この関数は常に1を返すようにしよう!
みたいなことができるようになります。

こうすれば、他の関数の影響を受けずに関数をテストする事ができます。

mockでできること

mockを使うとこんな事ができます。

  • 関数の戻り値を変える
  • 関数から例外を投げる
  • 関数が呼ばれた回数をカウントする
  • 関数が受け取った引数を記憶する

mockのインポート

mockは最初から入っているので、importするだけで使えます。

from unittest import mock

mockオブジェクトの作成

mockオブジェクトを作成する場合は、with文を使ったり、デコレータを使うのが一般的です。
今回はこの2つのやり方のみ扱います。

ちなみに、mockに置き換えられた関数は、何も設定しなければmockオブジェクトを返します。

with文を使った場合

with文を使うとその中だけ関数がmockに置き換わります。

import os
from unittest import mock

with mock.patch('os.path.join') as join_mock:
    os.path.join('dir','filename')  # mockオブジェクトを返す

デコレータを使った場合

デコレータを使った場合は、関数の中だけmockに置き換わります。

import os
from unittest import mock

@mock.patch('os.path.join')
def function(join_mock):
    os.path.join('dir','filename')  # mockオブジェクトを返す

モジュールの指定方法

さっきは'os.path.join'としましたが、ここの指定方法は慣れるまで結構厄介です。

patchの引数に渡す文字列は以下のことに気をつけてください。

  • importできる形になっている
  • クラス名、メソッド名、関数名になっている
  • 置き換えたい関数が含まれているモジュールを指定する

最後の部分が少しわかりにくいですね。。

例えば、以下のfunction.pyの中のfuncという関数をテストしたい場合、os.path.joincalcをmockにする必要があります。

# function.py
import os

def func():
    os.path.join('dir','filename')
    return 5 * calc(2, 3)

def calc(a, b):
    return a * (a + b)

この場合はこのように指定してあげます。

# test_function.py
from unittest import mock

@mock.patch('function.os.path.join')  # 2番目
@mock.patch('function.calc')  # 1番目
def test_func(calc_mock, join_mock):  # 1番目, 2番目
    func()

このように、mockにしてあげるときはfunctionの中の関数を指定します。os.path.joinとした場合は、function.pyの中のos.path.joinはmockになりません。

それと、2つ以上の場合は、patchの順番と引数の順番には注意しましょう。

クラスの場合

クラスの場合も同じようにできます。

# function.py
import os

class MyClass:
    def func():
        os.path.join('dir','filename')
        return 5 * calc(2, 3)

    def calc(a, b):
        return a * (a + b)

この場合はこのように指定してあげます。

# test_function.py
from function import MyClass
from unittest import mock

@mock.patch('function.os.path.join')  # 2番目
@mock.patch('function.MyClass.calc')  # 1番目
def test_func(calc_mock, join_mock):  # 1番目, 2番目
    MyClass().func()

ちなみにクラスまるごとmockに置き換える事もできます。

mockの戻り値を指定する

mockの戻り値の設定には色々な種類があります。

決められた値を返す

mockの戻り値を指定するにはreturn_valueに値を設定します。
これは、mockの戻り値を10に設定する例です。

mock_func.return_value = 10

もしくはpatchの引数として渡してやります。

@mock.patch('function.func', return_value=10)

呼ばれた回数で返す値を変える

この場合はside_effectを使います。
1回目は10、2回目は20、3回目は30を返すようになります。

mock_func.side_effect = [10, 20, 30]

テストする関数の中で複数回おなじ関数を使っているときは、このやり方で返す値を変えると良いです。

条件によって返す値を変える

条件によって返す値を返すにはside_effectに関数を入れます。
この例では、mockの引数に渡された値が+ならTrue、-ならFalseを返すようになります。

def side_effect_func(value):
    if value >= 0:
        return True
    else:
        return False

mock_func.side_effect = side_effect_func

このように受け取った引数によって返す値を変えたいときに使います。

関数の戻り値が別のクラスになってしまう場合に戻り値を設定する方法

例えばこんな場合にはどうやってmockにすればよいか悩んでしまいますね。

# function.py
import obj
def func():
    obj1 = obj.func1('aaa')
    obj2 = obj1.func2('bbb')

この場合は一旦objだけmockにします。
そして、関数をたどって変更したい関数のところで戻り値を設定します。
この例ではfunc2の戻り値を10に設定しています。

@mock.patch('function.obj')
def test_func(mock_obj):
    mock.func1.return_value.func2.return_value =10

ちょっと長くてわかりにくいですが、一つずつ追っていけばできると思います。

mockから例外を投げる

mockから例外を投げるようにもできます。
この場合はside_effectに例外クラスを渡すだけです。

mock_func.side_effect = Exception('message')

これを使えば、例外発生時のテストも簡単にできます。

mockが呼ばれた回数を調べる

mockには、呼ばれた回数を調べる機能もあります。
これは、mockを設定して、テストしたい関数を実行したあとに、期待した動作をしたかどうかを調べるために使います。

呼ばれた回数を取得する

あらかじめ呼ばれる回数がわかっているなら、これでテストすることができます。

mock_func.call_count

1回以上呼ばれたことをアサートする

これは少なくとも一回以上mockが呼ばれていたらテストをパスします。

mock_func.assert_called()

1回だけ呼ばれたことをアサートする

これは1回だけ呼ばれた場合にテストをパスします。

mock_func.assert_called()

1回だけ呼ばれたことをアサートする

これは1回だけ呼ばれた場合にテストをパスします。

mock_func.assert_called()

1回も呼ばれなかったことをアサートする

これは1回も呼ばれなかった場合にテストをパスします。

mock_func.assert_not_called()

mockに渡された引数を調べる

最後に呼ばれた引数をアサートする

最後に呼ばれた引数が期待した引数と一致した場合にテストをパスします。
この場合は、モック化した関数が最後に引数(1, 2, 3, test='aaa')で呼ばれていたらテストをパスします。

mock_func.assert_called_with(1, 2, 3, test='aaa')

指定した引数で一度だけ呼ばれたかをアサートする

これは期待した引数で一度だけ呼ばれたかどうかをテストするためのものです。

mock_func.assert_called_once_with(1, 2, 3, test='aaa')

指定した引数で呼ばれたかをアサートする

これは、一度でも指定した引数で呼ばれていればテストをパスします。

mock_func.assert_any_call(1, 2, 3, test='aaa')

期待した引数がすべて呼び出されたかをアサートする

これは、複数回関数が呼び出されたときに、そのすべての引数が期待したものかどうかをテストできます。
これを使うときはcallをインポートしておきます。

from unittest.mock import call

calls = [call(1, test='aaa'), call(2, test='aaa')]
mock_func.assert_any_call(calls)
mock_func.assert_any_call(calls, any_order=True)

callsに呼び出されたすべての引数がなくてもテストはパスします。
逆にcallsに含まれている引数で関数が呼び出されていなければテストはパスしません。

any_orderをTrueにすると、順番が関係なくなります。
Falseにすると、呼び出された順番も一致していないといけません。
さらに、間で別の引数で呼ばれていた場合もテストはパスしません。

mockが呼び出されたときの引数をすべて取得する

引数をすべて取得したい場合はこれを使います。

mock_func.call_args_list

これはcallオブジェクトのリストで値を返します。
なので、(1, test='aaa')(2, test='aaa')で呼ばれていた場合はこのような値が返ってきます。

[call(1, test='aaa'), call(2, test='aaa')]

mockの呼び出し情報をクリアする

呼び出し情報をクリアする場合はこのメソッドを使います。

mock_func.reset_mock()

最後に

今回はmockについてまとめてみました。

  • 関数の戻り値を変える
  • 関数から例外を投げる
  • 関数が呼ばれた回数をカウントする
  • 関数が受け取った引数を記憶する

いろいろな使い方があってややこしいですが、どんなことができるかを覚えておくだけでも、後で調べるときに役に立つと思います。

ぜひ参考にしてみてください^^

コメント

タイトルとURLをコピーしました