반응형

4. Decoupling Units with unittest.mock

반응형

- Functions and methods that do not rely on the behavior of other functions, methods, or data are rare.

- Every one of these calls and instantiations breaks the unit's isolation.


Mock objects in general

- mock objects are any objects that you can use as substitutes in your test code, to keep your tests from overlapping and your tested code from infiltrating the wrong tests.

- in one pattern, you can create a mock object and perform all of the expected operations on it. 

- in the second pattern, you can create a mock object, do the minimal necessary configuration to allow it to mimic the real object it replaces, and pass it to your code.


Mock objects according to unittest.mock

- you can create a mock object, do the minimal necessary configuration to allow it to mimic the real object it replaces, and pass it to your code

=> This is what unittest.mock pursues.


Standard mock objects

- When you access an unknown attribute of a mock object, instead of raising an AttributeError exception, the mock object creates a child mock object and returns that.

- This always holds true: if you access the same attribute of a mock object, you'll get the same mock object back as the result. The next thing to notice might seem more surprising. Whenever you call a mock object, you get the same mock object back as the return value.

- As you can see in the previous example, while call('Foo', 1, 1) will match a call to the parent mock object, if the call used these parameters, call.x('foo', 1,1), it matches a call to the child mock object named x.

- Each call specification is only concerned with the parameters of the object that was finally called after all of the lookups.


Non-mock attributes

>>> del mock.w

>>> mock.w

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "/usr/lib64/python3.4/unittest/mock.py", line 563, in __getattr__

raise AttributeError(name)

AttributeError: w


Non-mock return values and raising exceptions

To make a mock object always return the same value, just change the

return_value attribute:

>>> mock.o.return_value = 'Hi'

>>> mock.o()

'Hi'

>>> mock.o('Howdy')

'Hi'


>>> mock.p.side_effect = [1, 2, 3]

>>> mock.p()

1

>>> mock.p()

2

>>> mock.p()

3

>>> mock.p()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "/usr/lib64/python3.4/unittest/mock.py", line 885, in __call__

return _mock_self._mock_call(*args, **kwargs)

File "/usr/lib64/python3.4/unittest/mock.py", line 944, in _mock_call

result = next(effect)

StopIteration


>>> mock.p.side_effect = itertools.count()


Mocking class or function details

- The unitest.mock package addresses this problem using a technique called speccing.

>>> from unittest.mock import create_autospec

>>> x = Exception('Bad', 'Wolf')

>>> y = create_autospec(x)

>>> isinstance(y, Exception)

True

>>> y

<NonCallableMagicMock spec='Exception' id='140440961099088'>


Mocking function or method side effects

- if you assign a function to a mock object's side_effect attribute, this mock object in effect becomes that function with the only important difference being that the mock object still records the details of how it's used.


Mocking containers and objects with a special behavior

- the Mock class does not handle is the so-called magic methods that underlie Python's special syntactic constructions: __getitem__, __add__, and so on.

>>> from unittest.mock import MagicMock

>>> mock = MagicMock()

>>> 7 in mock

False

>>> mock.mock_calls

[call.__contains__(7)]

>>> mock.__contains__.return_value = True

>>> 8 in mock

True

>>> mock.mock_calls

[call.__contains__(7), call.__contains__(8)]

Things work similarly with the other magic methods. For example, addition:

>>> mock + 5

<MagicMock name='mock.__add__()' id='140017311217816'>

>>> mock.mock_calls

[call.__contains__(7), call.__contains__(8), call.__add__(5)]


- Python ensures that some magic methods return a value of a particular type, and will raise an exception if that requirement is not fulfilled.


>>> mock = MagicMock()

>>> x = mock

>>> x += 5

>>> x

<MagicMock name='mock.__iadd__()' id='139845830142216'>

>>> x += 10

>>> x

<MagicMock name='mock.__iadd__().__iadd__()' id='139845830154168'>

>>> mock.mock_calls

[call.__iadd__(5), call.__iadd__().__iadd__(10)]


Mock objects for properties and descriptors

- Descriptors are objects that allow you to interfere with the normal variable access mechanism.

- The only complication is that you can't assign a descriptor to an object instance; you have to assign it to the object's type because descriptors are looked up in the type without first checking the instance.


>>> from unittest.mock import PropertyMock

>>> mock = Mock()

>>> prop = PropertyMock()

>>> type(mock).p = prop

>>> mock.p

<MagicMock name='mock()' id='139845830215328'>

>>> mock.mock_calls

[]

>>> prop.mock_calls

[call()]

>>> mock.p = 6

>>> prop.mock_calls

[call(), call(6)]


- Each Mock you create has its own type object, even though they all claim to be of the same class


Mocking file objects

- The unittest.mock library helps you with this by providing mock_open, which is a factory for fake open functions.

>>> from unittest.mock import mock_open

>>> open = mock_open(read_data = 'moose')

>>> with open('/fake/file/path.txt', 'r') as f:

... print(f.read())

...

moose


Replacing real code with mock objects

- patch dropped the mock open function created by mock_open over the top of the real open function; then, when we left the context, it replaced the original for us automatically.

>>> from unittest.mock import patch, mock_open

>>> with patch('builtins.open', mock_open(read_data = 'moose')) as mock:

... with open('/fake/file.txt', 'r') as f:

... print(f.read())

...

moose

>>> open

<built-in function open>


>>> import io

>>> with patch('io.BytesIO'):

... x = io.BytesIO(b'ascii data')

... io.BytesIO.mock_calls

[call(b'ascii data')]


- The patch function creates a new MagicMock for you if you don't tell it what to use for the replacement object.


>>> with patch('io.BytesIO', autospec = True):

... io.BytesIO.melvin

Traceback (most recent call last):

File "<stdin>", line 2, in <module>

File "/usr/lib64/python3.4/unittest/mock.py", line 557, in __getattr__

raise AttributeError("Mock object has no attribute %r" % name)

AttributeError: Mock object has no attribute 'melvin'


Better PID tests


Patching time.time


*** What are the benefits of mocking?
Increased speed, Avoiding undesired side effects during testing.

반응형

이 글을 공유하기

댓글

Designed by JB FACTORY