0

I have an uninitialzed global variable in my module that is properly initialized during application startup. For typechecking I use the syntax var: Type without a value from python >= 3.6 so I do not have to complicate the typechecking for the default value (which would be None).

Now I want to unittest another function that uses this global variable but I get an error from unittest.mock.patch. Here is a simplified version of what I am doing:

  • File mod.py:
global_var: bool
#global_var: bool = False
def init(val: bool) -> None:
    global global_var
    global_var = val
def do_stuff() -> str:
    return "a" if global_var else "b"
  • File test.py:
import unittest.mock, mod
class MockUninitializedGlobal(unittest.TestCase):
    def test_it(self):
        with unittest.mock.patch("mod.global_var", True):
            actual = mod.do_stuff()
        expected = "a"
        self.assertEqual(expected, actual)
  • I run it with python3 -m unittest test.py.

The exception is

Traceback (most recent call last):
  File "/home/luc/soquestion/test.py", line 4, in test_it
    with mock.patch("mod.global_var", True):
  File "/nix/store/vs4vj1yzqj1bkcqkf3b6sxm6jfy1gb4j-python3-3.7.7/lib/python3.7/unittest/mock.py", line 1323, in __enter__
    original, local = self.get_original()
  File "/nix/store/vs4vj1yzqj1bkcqkf3b6sxm6jfy1gb4j-python3-3.7.7/lib/python3.7/unittest/mock.py", line 1297, in get_original
    "%s does not have the attribute %r" % (target, name)
AttributeError: <module 'mod' from '/home/luc/soquestion/mod.py'> does not have the attribute 'global_var'

If I comment line 1 and uncomment line 2 in mod.py the test passes.

Is there any way to make the test pass without definig a default value for the global variable in my application code?


Edit: As noted in the comments the actual variable is a parsed config file which I do not want to load during tests. It is used in a command line application and therefore the variable is always set after command line parsing is done. The real code is here and here. In the actual test I am not trying to mock the global variable as a bool directly but some attribute on the config object.
Lucas
  • 685
  • 4
  • 19
  • run the `init` function first, no? – gold_cy Apr 13 '20 at 18:03
  • 1
    Mocking `mod.global_var` will never work if that name does not exist when you execute the `with` statement. Since modules are also objects, you could "inject" that value by adding `mod.global_var = True` before and `del mod.global_var` after your `with` statement, but that's a hacky workaround. You should either run `init` beforehand as @gold_cy suggests or rethink your design - what do you gain from writing `global_var: bool` instead of `global_var: bool = False`? – jfaccioni Apr 13 '20 at 18:10
  • In the real application the variable is called `config` and holds an instance of my parsed config file (which is represented by a custom class). I do not want to run `init()` and parse a config file in this unit test. I also do not want to set `config: Optional[Config] = None` in my module. Because for the real application and for typechacking I know that it will always be an instance of the `Config` class and never be used uninitialzed, and the type checking code would get more ugly if it had to handle the unnecessary `None` case. – Lucas Apr 13 '20 at 19:25
  • Well, if you don't want to use `Optional` (which I would prefer), you only have the possibility to initialize with some default-constructed light-weight config, or to add the variable directly to the module (the hack described by @jfaccioni). – MrBean Bremen Apr 14 '20 at 12:53

1 Answers1

0

I used the setUp and tearDown methods on the unittest.TestCase class to solve this.

With my edit above about it actually being a config object the code looks like this:

class Config:
    def __init__(self, filename: str):
        "Load config file and set self.stuff"
        self.stuff = _code_to_parse(filename)
config: Config
def do_stuff() -> str:
    return "a" if config.stuff else "b"

And the test creates a mocked config object before the tests in order to mock some attribute on the config object during the test. In the teardown I also delete the object again so that nothing will spill to the next test case.

The actual mocking of the attributes I need is then done in a with block in the actual test as it is (a) cleaner and (b) I can just mock what I need in contrast to mocking all attributes on the config object in the setUp method.

import unittest, unittest.mock, mod
class MockUninitializedGlobal(unittest.TestCase):
    def setUp(self):
        mod.config = unittest.mock.Mock(spec=mod.Config)
    def tearDown(self):
        del mod.config
    def test_it(self):
        with unittest.mock.patch("mod.config.stuff", True):
            actual = mod.do_stuff()
        expected = "a"
        self.assertEqual(expected, actual)
Lucas
  • 685
  • 4
  • 19