2016/09/10

Python, Mock, Unittest and missing module dependency

Sometimes you end up in a situation where you don't have access to a depending module. It might be that it's only available in the target environment. Or, for some unknown reason the API libs are just not there on the company CI system.  

How to unit test the code without having the depending modules available at all?


This happens often when you work with API libraries, which are provided as a part of an installed product. And worst of all, the development kits are only provided for the certain OS - but not for yours.

Unit testing and mocking is less painful when the API is based on objects and classes. If the depending module is static and has a state, unit tests end up being a bit of a mess. 

Unit testing without the depending module


Luckily with the Python it's possible to work out these issues in several different ways. Here I present my solution. It took a while for me to figure out all the details, so I hope this proves useful for someone. 

Assuming the following is the code you've created. You don't have the missing_module implementation around for the unit tests.

mocked.py
_____________________________________________

import missing_module

class ClassWithDependency:


    def call_dependency(self):

        try:
            return missing_module.things.do()
        except missing_module.Error as ne:
            raise RuntimeError()
_____________________________________________

Writing the unit tests without the depending module


The code above is simple, but writing unit tests is slightly challenging for several reasons:

  • There is no missing_module to import in test unit.
  • Mocked methods of missing_module must be implemented one by one without the original module. If you had the missing_module in hand, you could just partially mock some parts of the module.
  • If the missing_module is a static module, the mock must be 'reset' manually after each test.
  • When the missing_module has a custom Exceptions as well, things get especially awful. This is because the 'try-catch' of the Python uses isinstance() and your mock's parent class is not BaseException, which the Python standard assumes. And we can't use "spec=...." to fix that issue, as we don't have the missing_module in hand.


The solution for unit testing the code is to do some tricks to work around the issue of lack of missing_module. Comments inline.

test_mocked.py
_____________________________________________

import unittest
import mock
# We must mock the import, as we don't have the module available
import sys
sys.modules['missing_module'] = mock.MagicMock()
import mocked

class TestElementCases(unittest.TestCase):

    
    # A convenience hook for tests
    missing = sys.modules['missing_module']

    def tearDown(self):

        # It's a static module, so these must be cleared
        # on every test
        self.missing.things.do.side_effect = None
        self.missing.things.do.return_value = None
    
   def test_call_dependecy_successful(self):
         # Return value for missing_module functions
         # through the hook. Any other mock methods and
         # variables can be used through it as well
         # (eg. 'called')
         self.missing.things.do.return_value = "Bar"
         dependency = mocked.ClassWithDependency()

         # Success

         self.assertEqual(dependency.call_dependency(), "Bar")

    # Patch must be set as we have to work around the isinstance().

    # Default Exception satisfies the lookup. 
    # Lambda and new_callable returns
    # the implementation instead of the one which we don't have
    @mock.patch('mocked.missing_module.Error', 
                new_callable=lambda: Exception)
    def test_call_dependecy_exception(self,stub_excption):
        self.missing.things.do.side_effect = stub_excption()
        dependency = mocked.ClassWithDependency()
       
        # The custom exception was thrown Succesfully 
        # (the code converts that to RTE), but
        # that's just a nuance
        with self.assertRaises(RuntimeError) as rer:
            dependency.call_dependency()
_____________________________________________


That's it. Now every line of mocked.py is getting covered 100% in unit tests without the missing_module


You're good to go.

No comments:

Post a Comment