简体   繁体   中英

Testing that a method in instance has been called in mock

I have this sort of setup where I'm testing a class which is using another class, and I want to mock the latter so I'm only testing the first class itself.

nuclear_reactor.py :

class NuclearReactor():
    def __init__(self):
        print "initializing the nuclear reactor"

    def start(self):
        print "starting the nuclear reactor"

nuclear_manager.py :

from nuclear_reactor import NuclearReactor

class NuclearManager():
    def __init__(self):
        print "manager creating the nuclear reactor"
        self.reactor = NuclearReactor()

    def start(self):
        print "manager starting the nuclear reactor"
        self.reactor.start()

test_nuclear_manager.py :

from mock import Mock
import nuclear_manager
from nuclear_manager import NuclearManager

def test():
    mock_reactor = nuclear_manager.NuclearReactor = Mock()
    nuke = NuclearManager()
    nuke.start()
    nuke.start()
    print mock_reactor.mock_calls
    print mock_reactor.start.call_count

test()

What I'd like to test is that NuclearReactor.start is called, but when I run this I get:

manager creating the nuclear reactor
manager starting the nuclear reactor
manager starting the nuclear reactor
[call(), call().start(), call().start()]
0

Which I totally understand since start is an attribute of the instance and not of the class, and I could parse the mock_calls , but isn't there a better way to check that the call of an instantiated mocked class is made?

I could use dependency injection in NuclearManager to pass a mock NuclearReactor , but I'm thinking there would be an alternative way using just mock.

You are indeed testing if start has been called directly on the class , which your code does not. You can test the method on the instance directly; remember that an instance is produced by calling the class:

print mock_reactor.return_value.calls
print mock_reactor.return_value.start.call_count

The Mock.return_value attribute is the result of the call to the mocked class, so the instance.

You can also just call the mock. Mocks by default always return the exact same object when called, a new mock representing that return value:

print mock_reactor().calls
print mock_reactor().start.call_count

The result of calling a mock instance, and the mock instance return_value attribute, are one and the same.

You were already on the right path by printing out the calls to the NuclearReactor mock, you just missed the detail that start() was invoked on the called mock, so call().start() , not start() was recorded.

You may want to use mock.patch() to handle the patching, rather than by direct assignment; this makes sure that the patch is removed again so that other tests can make their own decisions on what is mocked:

import mock
from nuclear_manager import NuclearManager

@mock.patch('nuclear_manager.NuclearReactor')
def test(mock_reactor):
    nuke = NuclearManager()
    nuke.start()
    nuke.start()

    instance = mock_reactor.return_value
    assert instance.start.call_count == 2
    instance.assert_called()

I used it as a decorator here; when the test() function is called, the mock is put in place, and when the function exits, it is removed again. You can also use patch() as a context manager to limit the scope of the patch even more finely.

Also, for unit testing like this, do use the unittest library :

import mock
import unittest
import nuclear_manager

class NuclearManagerTests(unittest.TestCase):
    @mock.patch('nuclear_manager.NuclearReactor')
    def test_start(self, mock_reactor):
        nuke = NuclearManager()
        nuke.start()
        nuke.start()

        instance = mock_reactor.return_value
        self.assertEqual(instance.start.call_count, 2)
        instance.assert_called()

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

This lets you fit your tests into a larger test suite, enable and disable tests, and integrate with other testing tools.

The way I use mocks is like this: (Code is Python 3)

from unittest.mock import MagicMock

class NuclearManager():
    def __init__(self, reactor):
        print("manager creating the nuclear reactor")
        self.reactor = reactor

    def start(self):
        print("manager starting the nuclear reactor")
        self.reactor.start()


def test():
    mock_reactor = MagicMock()
    nuke = NuclearManager(mock_reactor)
    nuke.start()
    nuke.start()

    # These two prints would actually be asserts of some sort
    print(mock_reactor.mock_calls)
    print(mock_reactor.start.call_count)

test()

Output:

manager creating the nuclear reactor
manager starting the nuclear reactor
manager starting the nuclear reactor
[call.start(), call.start()]
2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM