Home > OS >  Mock context manager inside a class
Mock context manager inside a class

Time:12-21

I have the below context manager inside a class method, which I would like to mock for unit testing.

def load_yaml_config(self) -> dict:
    """
    Load the config based on the arguments provided.

    Returns: dict
        dictionary which will be used for configuring the logger, handlers, etc.
    """

    with open(self.yaml_path, 'r') as config_yaml:
        return yaml.safe_load(config_yaml.read())

How could I achieve it?

EDIT: this is what I've tried so far, but it throws a warning because of "Shadows built-in name 'open'"

from unittest.mock import Mock, MagicMock

open = MagicMock()
yaml = Mock()
yaml.safe_load.return_value = 'My hardcoded value'

yaml_path = 'foo'


def mocked_yaml():
    with open(yaml_path, 'r') as config_yaml:
        return yaml.safe_load(config_yaml.read())


print(mocked_yaml())

CodePudding user response:

The easy way

Just create the appropriate yaml file in your testing code. But you probably don't want that, since you're making this post.

A hack with mocking

You can override open with your mock in the module scope:

# test_YourClass.py

builtin_open = open

class open:
    def __init__(self, *args, **kwargs):
        pass
    def __enter__(self):
        pass
    def __exit__(self, exc_type, exc_value, exc_traceback):
        pass
    def read(self):
        return 'hardcoded file contents for testing'

# Test here

open = builtin_open

This code is just a general idea, I haven't run it. It might require some additional work, such as parameterizing the mock file contents.

Dependency injection

The "proper" way is to unhardcode open() call in the class and inject your context manager, I suppose. It's up to you. I personally don't like injecting everything just for the purpose of unit testing.

CodePudding user response:

If you wanted to refactor this code to be able to test the safe_load part without having to actually open a file or patch builtins.open, you could do:

def load_yaml_config(self) -> dict:
    """
    Load the config based on the arguments provided.

    Returns: dict
        dictionary which will be used for configuring the logger, handlers, etc.
    """

    with open(self.yaml_path, 'r') as config_yaml:
        return self._load_yaml_config(config_yaml.read())

def _load_yaml_config(self, yaml_text: str) -> dict:
    return yaml.safe_load(yaml_text)

and then in your test:

TEST_YAML_DATA = """
stuff:
  other_stuff
"""

def test_load_yaml_config():
    assert WhateverMyClassIs()._load_yaml_config(TEST_YAML_DATA) == {
        'stuff': 'other_stuff'
    }

Modify to use actual appropriate YAML formatting and the correct expected dict output.

Note that all this is really testing is yaml.safe_load (which should have its own unit tests already) and the fact that your code calls it. Practically speaking I probably wouldn't bother covering this function in a unit test at all, but would instead try to have some sort of larger integration test (using a real file) that involved loading a config as part of some larger test scenario.

  • Related