web-dev-qa-db-ja.com

unittest setUpでpythonでモックを適切に使用する方法

TDDを学習する私の試みでは、単体テストを学習しようとし、Pythonでモックを使用しています。ゆっくりと慣れてきましたが、これを正しく行っているかどうかはわかりません。警告:ベンダーAPIがプリコンパイルされた2.4 pycファイルとして提供されているため、python 2.4を使用しているので、mock 0.8.0とunittest(unittest2ではありません)を使用しています

'mymodule.py'のこのサンプルコードを考える

import ldap

class MyCustomException(Exception):
    pass

class MyClass:
    def __init__(self, server, user, passwd):
        self.ldap = ldap.initialize(server)
        self.user = user
        self.passwd = passwd

    def connect(self):
        try:
            self.ldap.simple_bind_s(self.user, self.passwd)
        except ldap.INVALID_CREDENTIALS:
            # do some stuff
            raise MyCustomException

ここで、テストケースファイル「test_myclass.py」で、ldapオブジェクトをモックアウトします。 ldap.initializeはldap.ldapobject.SimpleLDAPObjectを返すので、それが私がモックアウトしなければならないメソッドになると考えました。

import unittest
from ldap import INVALID_CREDENTIALS
from mock import patch, MagicMock
from mymodule import MyClass

class LDAPConnTests(unittest.TestCase):
    @patch('ldap.initialize')
    def setUp(self, mock_obj):
        self.ldapserver = MyClass('myserver','myuser','mypass')
        self.mocked_inst = mock_obj.return_value

    def testRaisesMyCustomException(self):
        self.mocked_inst.simple_bind_s = MagicMock()
        # set our side effect to the ldap exception to raise
        self.mocked_inst.simple_bind_s.side_effect = INVALID_CREDENTIALS
        self.assertRaises(mymodule.MyCustomException, self.ldapserver.connect)

    def testMyNextTestCase(self):
        # blah blah

いくつか質問があります:

  1. それは正しく見えますか? :)
  2. それは、テストしているクラス内でインスタンス化されるオブジェクトを試して模擬する適切な方法ですか?
  3. SetUpで@patchデコレータを呼び出しても大丈夫ですか?これは奇妙な副作用を引き起こすでしょうか?
  4. とにかく、テストケースファイルに例外をインポートすることなく、ldap.INVALID_CREDENTIALS例外を発生させるモックを取得する方法はありますか?
  5. 代わりにpatch.object()を使用する必要がありますか?

ありがとう。

53
sjmh

patch()は、関数デコレータとしてだけでなく、クラスデコレータとしても使用できます。その後、以前のようにモック関数を渡すことができます。

@patch('mymodule.SomeClass')
class MyTest(TestCase):

    def test_one(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)

参照: 26.5.3.4。すべてのテストメソッドに同じパッチを適用する (代替方法もリストされています)

すべてのテストメソッドに対してパッチを適用する場合は、setUpでこの方法でパッチを設定する方が合理的です。

56
jooks

多くのパッチを適用し、setUpメソッドで初期化されたものにも適用したい場合は、これを試してください:

def setUp(self):
    self.patches = {
        "sut.BaseTestRunner._acquire_slot": mock.Mock(),
        "sut.GetResource": mock.Mock(spec=GetResource),
        "sut.models": mock.Mock(spec=models),
        "sut.DbApi": make_db_api_mock()
    }

    self.applied_patches = [mock.patch(patch, data) for patch, data in self.patches.items()]
    [patch.apply for patch in self.applied_patches]
    .
    . rest of setup
    .


def tearDown(self):
    patch.stopall()
10
Danny Staple

最初に質問に答えてから、patch()setUp()がどのように相互作用するかの詳細な例を示します。

  1. 私はそれが正しく見えるとは思わない、詳細についてはこのリストの質問#3への私の答えを見てください。
  2. はい、patchの実際の呼び出しは、必要なオブジェクトをモックするように見えます。
  3. いいえ、@patch()setUp()デコレータを使用することはほとんどありません。オブジェクトはsetUp()で作成され、テストメソッド中に作成されることはないため、ラッキーになりました。
  4. 例外をテストケースファイルにインポートせずに、モックオブジェクトに例外を発生させる方法は知りません。
  5. ここにpatch.object()の必要性は見当たりません。ターゲットを文字列として指定する代わりに、オブジェクトの属性にパッチを適用するだけです。

質問#3への私の答えを拡張すると、問題は、patch()デコレーターが装飾された関数の実行中にのみ適用されることです。 setUp()が戻るとすぐに、パッチは削除されます。あなたの場合、それは機能しますが、このテストを見ている人を混乱させるに違いないでしょう。 setUp()の間にのみパッチを実行したい場合は、withステートメントを使用して、パッチが削除されることを明確にすることをお勧めします。

次の例には、2つのテストケースがあります。 TestPatchAsDecoratorは、クラスを装飾すると、テストメソッド中にパッチが適用されますが、setUp()中には適用されないことを示しています。 TestPatchInSetUpは、setUp()とテストメソッドの両方の間にパッチを適用する方法を示しています。 self.addCleanUp()を呼び出すと、tearDown()の間にパッチが確実に削除されます。

import unittest
from mock import patch


@patch('__builtin__.sum', return_value=99)
class TestPatchAsDecorator(unittest.TestCase):
    def setUp(self):
        s = sum([1, 2, 3])

        self.assertEqual(6, s)

    def test_sum(self, mock_sum):
        s1 = sum([1, 2, 3])
        mock_sum.return_value = 42
        s2 = sum([1, 2, 3])

        self.assertEqual(99, s1)
        self.assertEqual(42, s2)


class TestPatchInSetUp(unittest.TestCase):
    def setUp(self):
        patcher = patch('__builtin__.sum', return_value=99)
        self.mock_sum = patcher.start()
        self.addCleanup(patcher.stop)

        s = sum([1, 2, 3])

        self.assertEqual(99, s)

    def test_sum(self):
        s1 = sum([1, 2, 3])
        self.mock_sum.return_value = 42
        s2 = sum([1, 2, 3])

        self.assertEqual(99, s1)
        self.assertEqual(42, s2)
10
Don Kirkby

new引数がpatch()デコレータに渡される受け入れられた答えのバリエーションを指摘したいと思います。

_from unittest.mock import patch, Mock

MockSomeClass = Mock()

@patch('mymodule.SomeClass', new=MockSomeClass)
class MyTest(TestCase):
    def test_one(self):
        # Do your test here
_

この場合、すべてのテストメソッドに2番目の引数MockSomeClassを追加する必要がなくなり、多くのコードの繰り返しを節約できることに注意してください。

この説明は https://docs.python.org/3/library/unittest.mock.html#patch にあります。

patch()がデコレーターとして使用され、newが省略された場合、作成されたモックは追加の引数として装飾された関数に渡されます。 。

上記の答えはすべて省略していますnewが、それを含めると便利です。

4
Kurt Peek

パッチを適用した内部関数を作成し、setUpから呼び出すことができます。

元のsetUp関数が次の場合:

def setUp(self):
    some_work()

次に、次のように変更してパッチを適用できます。

def setUp(self):
    @patch(...)
    def mocked_func():
        some_work()

    mocked_func()
0
Karpad