I am trying to understand the difference between these two approaches of mocking a method. Could someone please help distinguish them? For this example, I use the passlib library.
from passlib.context
import CryptContext
from unittest
import mock
with mock.patch.object(CryptContext, 'verify', return_value = True) as foo1:
mycc = CryptContext(schemes = 'bcrypt_sha256')
mypass = mycc.encrypt('test')
assert mycc.verify('tesssst', mypass)
with mock.patch('passlib.context.CryptContext.verify', return_value = True) as foo2:
mycc = CryptContext(schemes = 'bcrypt_sha256')
mypass = mycc.encrypt('test')
assert mycc.verify('tesssst', mypass)
Patch can be used as a TestCase class decorator. It works by decorating each test method in the class. This reduces the boilerplate code when your test methods share a common patchings set. patch() finds tests by looking for method names that start with patch.TEST_PREFIX. By default this is 'test', which matches the way unittest finds tests. You can specify an alternative prefix by setting patch.TEST_PREFIX.,All of the patchers can be used as class decorators. When used in this way they wrap every test method on the class. The patchers recognise methods that start with 'test' as being test methods. This is the same way that the unittest.TestLoader finds test methods by default.,The patch() decorator / context manager makes it easy to mock classes or objects in a module under test. The object you specify will be replaced with a mock (or other object) during the test and restored when the test ends:,Patching a class replaces the class with a MagicMock instance. If the class is instantiated in the code under test then it will be the return_value of the mock that will be used.
>>> from unittest.mock
import MagicMock
>>>
thing = ProductionClass() >>>
thing.method = MagicMock(return_value = 3) >>>
thing.method(3, 4, 5, key = 'value')
3
>>>
thing.method.assert_called_with(3, 4, 5, key = 'value')
>>> mock = Mock(side_effect = KeyError('foo')) >>>
mock()
Traceback(most recent call last):
...
KeyError: 'foo'
>>> values = {
'a': 1,
'b': 2,
'c': 3
} >>>
def side_effect(arg):
...
return values[arg]
...
>>>
mock.side_effect = side_effect >>>
mock('a'), mock('b'), mock('c')
(1, 2, 3) >>>
mock.side_effect = [5, 4, 3, 2, 1] >>>
mock(), mock(), mock()
(5, 4, 3)
>>> from unittest.mock
import patch
>>>
@patch('module.ClassName2')
...@patch('module.ClassName1')
...def test(MockClass1, MockClass2):
...module.ClassName1()
...module.ClassName2()
...assert MockClass1 is module.ClassName1
...assert MockClass2 is module.ClassName2
...assert MockClass1.called
...assert MockClass2.called
...
>>>
test()
>>> with patch.object(ProductionClass, 'method', return_value = None) as mock_method:
...thing = ProductionClass()
...thing.method(1, 2, 3)
...
>>>
mock_method.assert_called_once_with(1, 2, 3)
>>> foo = {
'key': 'value'
} >>>
original = foo.copy() >>>
with patch.dict(foo, {
'newkey': 'newvalue'
}, clear = True):
...assert foo == {
'newkey': 'newvalue'
}
...
>>>
assert foo == original
For our first example, we’ll refactor a standard Python test case from original form to one using mock. We’ll demonstrate how writing a test case with mocks will make our tests smarter, faster, and able to reveal more about how the software works.,One of the first things that should stick out is that we’re using the mock.patch method decorator to mock an object located at mymodule.os, and injecting that mock into our test case method. Wouldn’t it make more sense to just mock os itself, rather than the reference to it at mymodule.os?,The mock library has a special method decorator for mocking object instance methods and properties, the @mock.patch.object decorator:,Mocking in Python means the unittest.mock library is being utilized to replace parts of the system with mock objects, allowing easier and more efficient unit testing than would otherwise be possible.
We all need to delete files from our filesystem from time to time, so let’s write a function in Python which will make it a bit easier for our scripts to do so.
#!/usr/bin/env python
# - * -coding: utf - 8 - * -
import os
def rm(filename):
os.remove(filename)
Let’s write a traditional test case, i.e., without mocks:
#!/usr/bin/env python
# - * -coding: utf - 8 - * -
from mymodule
import rm
import os.path
import tempfile
import unittest
class RmTestCase(unittest.TestCase):
tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile")
def setUp(self):
with open(self.tmpfilepath, "wb") as f:
f.write("Delete me!")
def test_rm(self):
# remove the file
rm(self.tmpfilepath)
# test that it was actually removed
self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")
Let’s refactor our test case using mock
:
#!/usr/bin/env python
# - * -coding: utf - 8 - * -
from mymodule
import rm
import mock
import unittest
class RmTestCase(unittest.TestCase):
@mock.patch('mymodule.os')
def test_rm(self, mock_os):
rm("any path")
# test that rm called os.remove with the right parameters
mock_os.remove.assert_called_with("any path")
Great. Now, let’s adjust our test case to keep coverage up.
#!/usr/bin/env python
# - * -coding: utf - 8 - * -
from mymodule
import rm
import mock
import unittest
class RmTestCase(unittest.TestCase):
@mock.patch('mymodule.os.path')
@mock.patch('mymodule.os')
def test_rm(self, mock_os, mock_path):
# set up the mock
mock_path.isfile.return_value = False
rm("any path")
# test that the remove call was NOT called.
self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")
# make the file 'exist'
mock_path.isfile.return_value = True
rm("any path")
mock_os.remove.assert_called_with("any path")
We’ll begin with a refactor of the rm
method into a service class. There really isn’t a justifiable need, per se, to encapsulate such a simple function into an object, but it will at the very least help us demonstrate key concepts in mock
. Let’s refactor:
#!/usr/bin/env python
# - * -coding: utf - 8 - * -
import os
import os.path
class RemovalService(object):
""
"A service for removing objects from the filesystem."
""
def rm(filename):
if os.path.isfile(filename):
os.remove(filename)