Wednesday, June 6, 2012

Patching tip using mocks in python unit tests

I use the mock library by Michael Foord in my python unit tests and one problem always plagued me.  Here's the problem and the solution.

Sometimes when I import a package/module in my code I use this pattern (let's call it pattern A):

"""file_module_pattern_a.py"""
import os

def get_files(path):
    """Return list of files"""
    return os.listdir(path)


Other times, I use this pattern (let's call it pattern B):

"""file_module_pattern_b.py"""
from os import listdir

def get_files(path):
    """Return list of files"""
    return listdir(path_variable)

Note the differente.  In pattern A, I import the whole os package, while in pattern B, I only import the listdir function.  Now in my unit tests, here's what I use for pattern A:

"""Unit tests for module file_module_pattern_a"""

from file_module_pattern_a import get_files
from unittest import TestCase
from mock import patch, sentinel

class StandloneTests(TestCase):
    """Test the standalone functions"""
    
    @patch('os.listdir')
    def test_get_files(self, mock_listdir):
        """Test the get_files function"""
        test_result = get_files(sentinel.PATH)
        mock_listdir.assert_called_once_with(sentinel.PATH)
        self.assertEqual(test_result, mock_listdir.return_value)

This works great.  The only problem is... if I use pattern B with this unit test, the mock_listdir never gets called.  The unit test tries to use the REAL os.listdir function.

Here's the issue at hand.  When I use pattern B, I'm actually adding the function to my module, not the global scope.  As a result, the patch directive needs to reference my module, not os.  Here's the correct unit test patch syntax:

"""Unit tests for module file_module_pattern_b"""

from file_module_pattern_b import get_files
from unittest import TestCase
from mock import patch, sentinel

class StandloneTests(TestCase):
    """Test the standalone functions"""
    
    @patch('file_module_pattern_b.listdir')
    def test_get_files(self, mock_listdir):
        """Test the get_files function"""
        test_result = get_files(sentinel.PATH)
        mock_listdir.assert_called_once_with(sentinel.PATH)
        self.assertEqual(test_result, mock_listdir.return_value)

3 comments:

  1. The only problem is that you get tied too much to the implementation details :/

    ReplyDelete
  2. Thanks! It did help me to understand why mocking the module I want does not work as I usually use the method B and most mock examples use method A without mentioning this pitfall

    ReplyDelete
  3. Thanks!! Great explainataion

    ReplyDelete