99

I have a function I want to unit test contains calls two other functions. I am unsure how can I mock both functions at the same time properly using patch. I have provided an example of what I mean below. When I run nosetests, the tests pass but I feel that there must be a cleaner way to do this and I do not really Understand the piece regarding f.close()...

The directory structure looks like this:

program/
  program/
    data.py
  tests/
    data_test.py

data.py:

import cPickle

def write_out(file_path, data):
    f = open(file_path, 'wb')
    cPickle.dump(data, f)
    f.close()

data_test.py:

from mock import MagicMock, patch

def test_write_out():
    path = '~/collection'
    mock_open = MagicMock()
    mock_pickle = MagicMock()
    f_mock = MagicMock()
    with patch('__builtin__.open', mock_open):
        f = mock_open.return_value
        f.method.return_value = path
        with patch('cPickle.dump', mock_pickle):
            write_out(path, 'data')
            mock_open.assert_called_once_with('~/collection', 'wb')
            f.close.assert_any_call()
            mock_pickle.assert_called_once_with('data', f)

Results:

$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.008s
OK
alecxe
  • 441,113
  • 110
  • 1,021
  • 1,148
cnodell
  • 1,063
  • 1
  • 7
  • 8

3 Answers3

139

You can simplify your test by using the patch decorator and nesting them like so (they are MagicMock objects by default):

from unittest.mock import patch

@patch('cPickle.dump')
@patch('__builtin__.open')
def test_write_out(mock_open, mock_pickle):
    path = '~/collection'
    f = mock_open.return_value
    f.method.return_value = path
    
    write_out(path, 'data')
    
    mock_open.assert_called_once_with('~/collection', 'wb')
    mock_pickle.assert_called_once_with('data', f)
    f.close.assert_any_call()

Calls to a MagicMock instance return a new MagicMock instance, so you can check that the returned value was called just like any other mocked object. In this case f is a MagicMock named 'open()' (try printing f).

Karol Zlot
  • 1,495
  • 1
  • 12
  • 26
Matti John
  • 17,419
  • 7
  • 36
  • 38
  • 12
    In your suggestion, you introduce two parameters, one for each mock. How does python know which is which? I failed to find the answer to this in the docs. – steps Sep 18 '13 at 15:20
  • 70
    The decorators are applied bottom-up and the order of the parameters need to match this. See here: http://www.voidspace.org.uk/python/mock/patch.html?highlight=stack#nesting-patch-decorators – Matti John Sep 19 '13 at 08:12
  • 1
    Yes, I've read this, but didn't find it clear enough. Thanks! – steps Sep 19 '13 at 08:27
  • 6
    Notice that parameters here are in opposite order in which patches were applied. – Shubham Chaudhary Apr 27 '15 at 19:14
  • What is `f.method.return_value = path` doing? – Jwan622 Sep 13 '19 at 17:37
  • It sets the return value for the method, so every time `f.method()` always returns `path` – Matti John Sep 16 '19 at 15:56
74

In addition to the response @Matti John you can also use patch inside function test_write_out:

from mock import MagicMock, patch

def test_write_out():
    path = '~/collection'
    with patch('__builtin__.open') as mock_open, \
            patch('cPickle.dump') as mock_pickle:

        f = mock_open.return_value
        ...
Alex Lisovoy
  • 5,287
  • 2
  • 25
  • 27
2

Here's a simple example on how to test raising ConflictError in create_collection function using mock:

import os
from unittest import TestCase
from mock import patch
from ..program.data import ConflictError, create_collection


class TestCreateCollection(TestCase):
    def test_path_exists(self):
        with patch.object(os.path, 'exists') as mock_method:
            mock_method.return_value = True

            self.assertRaises(ConflictError, create_collection, 'test')

Please, also see mock docs and Michael Foord's awesome introduction to mock.

alecxe
  • 441,113
  • 110
  • 1,021
  • 1,148
  • Thank you for even trying to help me out. This does help me, but I was more focused on how to mock multiple functions using patch. Unfortunately, my question did not make that clear. I have cleaned up the question now. – cnodell Apr 10 '13 at 08:58