Mock an entire module in python

前端 未结 2 1609
庸人自扰
庸人自扰 2020-12-19 01:59

I have an application that imports a module from PyPI. I want to write unittests for that application\'s source code, but I do not want to use the module from PyPI in those

相关标签:
2条回答
  • 2020-12-19 02:30

    If you want to dig into the Python import system, I highly recommend David Beazley's talk.

    As for your specific question, here is an example that tests a module when its dependency is missing.

    bar.py - the module you want to test when my_bogus_module is missing

    from my_bogus_module import foo
    
    def bar(x):
        return foo(x) + 1
    

    mock_bogus.py - a file in with your tests that will load a mock module

    from mock import Mock
    import sys
    import types
    
    module_name = 'my_bogus_module'
    bogus_module = types.ModuleType(module_name)
    sys.modules[module_name] = bogus_module
    bogus_module.foo = Mock(name=module_name+'.foo')
    

    test_bar.py - tests bar.py when my_bogus_module is not available

    import unittest
    
    from mock_bogus import bogus_module  # must import before bar module
    from bar import bar
    
    class TestBar(unittest.TestCase):
        def test_bar(self):
            bogus_module.foo.return_value = 99
            x = bar(42)
    
            self.assertEqual(100, x)
    

    You should probably make that a little safer by checking that my_bogus_module isn't actually available when you run your test. You could also look at the pydoc.locate() method that will try to import something, and return None if it fails. It seems to be a public method, but it isn't really documented.

    0 讨论(0)
  • 2020-12-19 02:48

    While @Don Kirkby's answer is correct, you might want to look at the bigger picture. I borrowed the example from the accepted answer:

    import pypilib
    
    def bar(x):
        return pypilib.foo(x) + 1
    

    Since pypilib is only available in production, it is not suprising that you have some trouble when you try to unit test bar. The function requires the external library to run, therefore it has to be tested with this library. What you need is an integration test.

    That said, you might want to force unit testing, and that's generally a good idea because it will improve the confidence you (and others) have in the quality of your code. To widen the unit test area, you have to inject dependencies. Nothing prevents you (in Python!) from passing a module as a parameter (the type is types.ModuleType):

    try:
        import pypilib     # production
    except ImportError:
        pypilib = object() # testing
    
    def bar(x, external_lib = pypilib):
        return external_lib.foo(x) + 1
    

    Now, you can unit test the function:

    import unittest
    from unittest.mock import Mock
    
    class Test(unittest.TestCase):
        def test_bar(self):
            external_lib = Mock(foo = lambda x: 3*x)
            self.assertEqual(10, bar(3, external_lib))
         
    
    if __name__ == "__main__":
        unittest.main()
    

    You might disapprove the design. The try/except part is a bit cumbersome, especially if you use the pypilib module in several modules of your application. And you have to add a parameter to each function that relies on the external library.

    However, the idea to inject a dependency to the external library is useful, because you can control the input and test the output of your class methods, even if the external library is not within your control. Especially if the imported module is stateful, the state might be difficult to reproduce in a unit test. In this case, passing the module as a parameter may be a solution.

    But the usual way to deal with this situation is called dependency inversion principle (the D of SOLID): you should define the (abstract) boundaries of your application, ie what you need from the outside world. Here, this is bar and other functions, preferably grouped in one or many classes:

    import pypilib
    import other_pypilib
    
    class MyUtil:
        """
        All I need from outside world
        """
        @staticmethod
        def bar(x):
            return pypilib.foo(x) + 1
    
        @staticmethod
        def baz(x, y):
            return other_pypilib.foo(x, y) * 10.0
    
        ...
        # not every method has to be static
    

    Each time you need one of these functions, just inject an instance of the class in your code:

    class Application:
        def __init__(self, util: MyUtil):
            self._util = util
            
        def something(self, x, y):
            return self._util.baz(self._util.bar(x), y)
    

    The MyUtil class must be as slim as possible, but must remain abstract from the underlying library. It is a tradeoff. Obviously, Application can be unit tested (just inject a Mock instead of an instance of MyUtil) while, under some circumstances (like a PyPi library not available during tests, a module that runs inside a framework only, etc.), MyUtil can be only tested within an integration test. If you need to unit test the boundaries of your application, you can use @Don Kirkby's method.

    Note that the second benefit, after unit testing, is that if you change the libraries you are using (deprecation, license issue, cost, ...), you just have to rewrite the MyUtil class, using some other libraries or coding it from scratch. Your application is protected from the wild outside world.

    Clean Code by Robert C. Martin has a full chapter on the boundaries.

    Summary Before using @Don Kirkby's method or any other method, be sure to define the boundaries of your application irrespective of the specific libraries you are using. This, of course, does not apply to the Python standard library...

    0 讨论(0)
提交回复
热议问题