1

TL;DR

unittest.TestCaseインスタンスを明示的に渡さずに、モック クラスのメソッド内から現在のインスタンスにアクセスできますか? TestCaseそうでない場合、そこから (インスタンス メソッドである)のアサーション ヘルパー関数にアクセスする適切な方法は何ですか? (いい方法はありますか?)

シナリオ:MagicMockメソッドに基づく独自のアサーション メソッドによるTestCase拡張

handle_multiple_foobars()ある関数 ( ) が別の関数 ( handle_one_foobar()) を正しく使用していることをテストしたいので、 をモックしてhandle_one_foobar()います。ここで「正しく」とは、各引数を個別にhandle_multiple_foobars()呼び出すことを意味します。handle_one_foobar()(引数handle_one_foobar()ごとに 1 つの呼び出しhandle_multiple_foobars()。) 呼び出しの順序は気にしません。

モックへの予想されるすべての呼び出しが行われたかどうかを確認する

したがって、私はこれから始めました:

import unittest
from unittest import TestCase
from unittest.mock import patch, call

def handle_one_foobar(foobar):
    raise NotImplementedError()

def handle_multiple_foobars(foobars):
    for foobar in reversed(foobars):
        handle_one_foobar(foobar)


class FoobarHandlingTest(TestCase):
    @patch('__main__.handle_one_foobar')
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
            self, handle_one_foobars_mock):
        foobars = ['foo', 'bar', 'foo', 'baz']

        handle_multiple_foobars(foobars)

        expected_calls = [call(fb) for fb in foobars]
        handle_one_foobars_mock.assert_has_calls(
                expected_calls,
                any_order=True)

if __name__ == '__main__':
    unittest.main()

モックへの予想される呼び出しのみが行われたかどうかを確認する

ただし、これは、モックされた関数への呼び出しがさらにある場合にも合格します。

def handle_multiple_foobars(foobars):
    handle_one_foobar('begin')
    for foobar in reversed(foobars):
        handle_one_foobar(foobar)
    handle_one_foobar('end')

私はそれをしたくありません。

追加のアサーション (または追加のテスト) を簡単に作成して、呼び出しの合計数を確認できます。しかし、これをテストする単一の条件として扱いたいと思います。だから私は別のアサーションを構築しました:

class FoobarHandlingTest(TestCase):
    @patch('__main__.handle_one_foobar')
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
            self, handle_one_foobar_mock):
        foobars = ['foo', 'bar', 'foo', 'baz']

        handle_multiple_foobars(foobars)

        expected_calls = [call(fb) for fb in foobars]
        self.assertCountEqual(
                handle_one_foobar_mock.mock_calls,
                expected_calls)

これにより、追加の呼び出しがうまくキャッチされます。

F
======================================================================
FAIL: test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar (__main__.FoobarHandlingTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.4/unittest/mock.py", line 1136, in patched
    return func(*args, **keywargs)
  File "convenientmock.py", line 23, in test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar
    expected_calls)
AssertionError: Element counts were not equal:
First has 1, Second has 0:  call('begin')
First has 1, Second has 0:  call('end')

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

新しいアサーションをモックのヘルパー メソッドに移動します

しかし、私の好みでは、ここで主張されていることは十分に明白ではありません。したがって、アサーションを新しい関数に抽出することにしました。この関数は と非常によく似た目的を果たすためassert_has_calls()、モック クラスのメソッドであるべきだと思います。拡張するのは難しくなくMagicMock、抽出されたメソッドをもう少し汎用的にして、呼び出し順序が重要かどうかを指定できるようにすることもできます。

from unittest.mock import MagicMock

class MyMock(MagicMock):
    def assert_has_exactly_calls(_mock_self, calls, any_order=False):
        tc = TestCase()
        asserter = tc.assertCountEqual if any_order else tc.assertEqual
        asserter(_mock_self.mock_calls, list(calls))

@patchunittest.mock.MagicMockテストメソッドの装飾を次のように変更すると、モックを作成する代わりにこのクラスを使用します

    @patch('__main__.handle_one_foobar', new_callable=MyMock)
    def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar( # ...

次に、アサーションを次のように書くことができます

        handle_one_foobar_mock.assert_has_exactly_calls(
                expected_calls,
                any_order=True)

...そして醜い

しかし、非常に醜いことに気づいたかもしれません:TestCaseインスタンス メソッドassertCountEqual()とを使用できるようにするために、テストが実行される実際のインスタンスではないassertEqual()ダミー インスタンスを作成しました。TestCaseFoobarHandlingTest

どうすればこれを回避できますか?

明らかに、テストselfをアサーション メソッドに渡すことはできますが、それは非常に非直感的なシグネチャになります。(テスト ケースについてアサーションを伝える必要があるのはなぜですか?)

4

0 に答える 0