简体   繁体   中英

How to mock external module calls and test code in the class body

I want to store static information in a class, shared by all the instances. The information is something obtained by using another module, I only want to do this once , eg. let's say that this is what I have in mymodule.py :

import os

MyClass:
    bus = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)

    def __init__(self):
        pass

How can I test this code and mock os.environ.get to make sure that the call is made correctly?

Since the execution happens at the time of the first import, I would need to reload the module in the test, but even so, I can't have os.environ.get mocked at the right time:

import unittest
from unittest import patch, MagicMock
import importlib
import mymodule

class TestMyClass(unittest.TestCase):

    @patch('mymodule.os.environ', spec=['get'])   
    def test_class_init_patch(self, mock_env):
        # Too early - the reload overrides the mock
        importlib.reload(mymodule)
        mock_env.get.assert_called_once_with('DBUS_SESSION_BUS_ADDRESS', None)

    def test_class_init_mock(self):
        importlib.reload(mymodule)
        # Too late - the class body already executed
        mymodule.MyClass.os.environ = MagickMock()

I have managed to come up with two alternatives, that make this testable:

  • Move the class initialization code into a class method and call it from the class body. I can test this class method in my unit test.

  • Move the class initialization into the __init__ method, guarded by a flag stored in a class variable so that it is only initialized once on the first instantiation.

While both of these should work just fine, it just feels more clean and straightforward to leave this in the class body if possible.

I have managed to figure out how to do this the right way (I hope)!

If you have imported only the module and not the class, function into your code's namespace, eg.:

  • like this: import os
  • and not like this: from os import geteuid

You can do this in your test to patch the os module directly:

import os
import unittest
from unittest import patch
import importlib
import mymodule

class TestMyClass(unittest.TestCase):

    @patch('os.environ.get')   
    def test_class_init_patch(self, mock_env_get):
        importlib.reload(mymodule)
        mock_env_get.assert_called_once_with('DBUS_SESSION_BUS_ADDRESS', None)

This is described in the official documentation as well:

However, consider the alternative scenario where instead of from a import SomeClass module b does import a and some_function uses a.SomeClass . Both of these import forms are common. In this case the class we want to patch is being looked up in the module and so we have to patch a.SomeClass instead:

More details in the official documentation.

This way the module is being patched directly and reloading your own module doesn't affect the patching.

Once the test has run, the patching of the module is undone as usual, however, keep in mind, that the state of your class remains the same way as it was initialized while the external module was patched, so you might need to reload the module again before you run your other tests.

The easiest way to make sure, that you reclaim your "normal" class state, is to reload the module in your TestCase object's setUp method, eg.:

def setUp(self):
    importlib.reload(mymodule)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM