1

I need to write test cases for the module

to_be_tested.py

from module_x import X

_x = X() # creating X instance in test environment will raise error

#.....

In the test case,

from unittest import TestCase, mock

class Test1(TestCase):

    @mock.patch('...to_be_tested._x')
    @mock.patch('...to_be_tested.X.func1')
    def test_1(self, mock_func1, mock_x):
        ...

However, this will not prevent the import from creating the instance. Is it a way to workaround it and write the test cases for the module? Or is it a way to refactory to_be_tested to be testable?

Maybe write in to_be_tested.py, just _x = None if detected test environment?

ca9163d9
  • 27,283
  • 64
  • 210
  • 413
  • If `to_be_tested.py` is under your control, modify it not to create an instance at import time but to delay that until first use. If `to_be_tested.py` is out of your control, see the solution here: [Mocking a module import in pytest](https://stackoverflow.com/q/43162722/674039) – wim Oct 02 '20 at 22:33
  • yes, I have full control of the source code now. I'm going to encapsulate the variable in a function: `_x = None / def get_x(): global _x / if _x == None: _x = X() / return _x`. Then other functions access `_x` using the function. Is this a good way? – ca9163d9 Oct 03 '20 at 05:43

2 Answers2

3

The instantiation of X at the global level seems problematic, but I don't have the full picture so I can't definitively say "don't do that". If you can refactor it so that the X() instance is created as needed or something along those lines, that would be ideal.

That said, here's a way to prevent module_x from being imported during the test. I'm basing that on the assumption that X() is used throughout the module_x module, so there's really no need for anything in that module and you only want to mock it.

import sys
import unittest

from unittest import TestCase, mock

class Test1(TestCase):

    @classmethod
    def setUpClass(cls):
        sys.modules['module_x'] = mock.Mock()

    @mock.patch('to_be_tested._x')
    @mock.patch('to_be_tested.X.func1')
    def test_1(self, mock_func1, mock_x):
        from to_be_tested import _x
        print(_x)

You can see that _x is a mock now, but note that you can't have the import outside of your tests (like at the top of the test module, like most imports usually are) because sys.modules['module_x'] hasn't been subbed out yet.

Dharman
  • 30,962
  • 25
  • 85
  • 135
wholevinski
  • 3,658
  • 17
  • 23
0

One possibility is to guard the creation of _x with an environment variable so that you can disable its initialization in test mode. For example,

import os
from module import X

_x = None
if 'TEST' not in os.environ:
    _x = X()

Now, you just need to ensure that TEST is set in your environment before importing to_be_tested. This is proabably something you would do in with your test runner, but it's also possible to do directly in your test module.

from unittest import TestCase, mock
import os


os.environ['TEST'] = ''

import to_be_tested

class Test1(TestCase):
    ...
chepner
  • 497,756
  • 71
  • 530
  • 681
  • You probably meant `if 'TEST' not in os.environ:`. This is a really ugly solution btw, and the module will likely already be imported one way or the other during the test discovery phase, so it may be too late to set in os.environ from within a test module. – wim Oct 02 '20 at 23:28
  • Test discovery is usually done by file name or from a directory of specific files, though like any convention there could be exceptions that make this approach fragile. – chepner Oct 03 '20 at 12:42
  • 1
    It would be cleaner to have `X` instantiated by a module-level function that needs to be called explicitly, but forcing the *production* user to call a function that the test code does not seems backwards. – chepner Oct 03 '20 at 12:43
  • In the end, this is more of a design issue than something to work around in testing. – chepner Oct 03 '20 at 12:44
  • If you instantiate `X` with a function call in production code like @chepner suggests, you can mock that function in the tests. – progmatico Nov 07 '20 at 17:24