Skip to main content

A Python package for failing and mocking imports in automated tests

Project description

Test Python imports

A Python package for failing and mocking imports in automated tests.

Note: This package was made with CPython in mind. There are no guarantees that it will work with other versions.

Content

  1. Failing imports
  2. Mocking imports

Failing imports

This was the original motive to create the package. I needed to test the behaviour of another package that had optional support for PIL, and I wanted the tests to check the behaviour both when the package is present and when it is not.

The usage is straightforward:

from test_imports import fail_imports


def f() -> bool:
    try:
        import PIL.Image
    except ImportError:
        return False
    else:
        return True


def test_success() -> None:
    assert f() is True


@fail_imports("PIL")
def test_decorator_fail() -> None:
    assert f() is False


def test_context_manager_fail() -> None:
    with fail_imports("PIL"):
        assert f() is False


test_success()
test_decorator_fail()
test_context_manager_fail()

All positional arguments in fail_imports are treated as the modules whose imports are to fail. Each of them can be:

  • a compiled regular expression, matched (using re.match, i.e., anchored at the beginning of the string) against names of the modules being imported; or

  • a string, which is matched literally, except for the asterisk, which is used to match any substring. The matching is done on complete strings.
    For example, "foo.bar*" will match foo.bar and foo.bard, but not foodbar (because dot is matched literally, not as in regular expressions). Further, "foo.*r" will match foo.bar, but not foo.bard because only the whole strings are matched. If you want foo.bard to match, the expression needs to be foo.*r*, or you can supply a compiled regular expression that would match it.

This function also supports some customisation through keyword-only arguments:

  • hide_modules is a sequence of module names matching patterns (as described above) that won't be failed, but will be removed from sys.modules, thus causing them to "reload". This helps test imports inside those modules because, if they are not "reloaded", their imports are not re-executed.

  • exception is either a class or an instance of the exception to be raised when an import fails. Unsurprisingly, this defaults to ModuleNotFoundError.

  • debug is a Boolean flag. If set to True, the package will produce extra prints in an attempt to help with its usage.

Mocking imports

Like more "normal" mocking, the mocking of modules is used to replace one object with another one, pretending to be the original. On the surface, mocking imports is easy:

from test_imports import mock_imports


def test_success() -> None:
    import math
    assert hasattr(math, "sin")
    assert not hasattr(math, "digits")


@mock_imports(math="string")
def test_decorator_fail() -> None:
    import math
    assert not hasattr(math, "sin")
    assert hasattr(math, "digits")


def test_context_manager_fail() -> None:
    with mock_imports(math="string"):
        import math
        assert not hasattr(math, "sin")
        assert hasattr(math, "digits")


test_success()
test_decorator_fail()
test_context_manager_fail()

However, mocking definitions are a bit more complicated than the ones for fail_imports.

First, there is a problem of mocking modules inside packages. One cannot do fail_imports(PIL.Image="mock_pil_image") because dots cannot be a part of arguments' names. Instead, we use double underscores (similar to, for example, Django):

with mock_imports(PIL__Image="math"):
    from PIL import Image
    assert not hasattr(Image, "new")
    assert hasattr(Image, "sin")

There is also a potential problem of collisions in names between the function's arguments and mocked modules. For example, there is a package debug, which we could not mock if debug was used as a keyword argument to turn on debugging outputs. That's why argument names are prefixed with "TI_" (so, TI_debug=True instead of debug=True).

Both of these can still cause potential conflicts. Some module can have double underscores in its name and some package's name could start with TI_. To account for these cases, mock_imports takes two positional-only arguments:

  • prefix is the prefix for keyword-only arguments that are recognised by this function. For example, if prefix is set to its default version "TI_", then the debugging value is assigned as TI_debug. Any names beginning with "TI_" that are not recognised as arguments are considered invalid.
    In other words, if you want to mock a module with a name starting with "TI_" (for example, TI_module), you need to change this prefix to something else and adjust keyword-arguments accordingly.

  • dot is the string used instead of dot in module names.

    So, these two calls are equivalent:

mock_imports(PIL__Image="mock_pil_image", TI_debug=True)
# and
mock_imports(
    "PREFIX_", "__xxx__", PIL__xxx__Image="mock_pil_image", PREFIX_debug=True,
)

This still does not allow matching with asterisk or with regular expressions, but it would hardly make sense to do so (mocking multiple different modules with the same one). However, if really needed, one can use the following Python "trick":

mock_imports(**{"tests.module*": math})

This will will load math instead of any module with a full name beginning with "tests.module".

The remaining arguments are keyword only (always prefix their names with prefix!):

  • hide_modules is as above: a sequence of module names matching patterns (as described above) that won't be failed, but will be removed from sys.modules, thus causing them to "reload". This helps test imports inside those modules because, if they are not "reloaded", their imports are not re-executed.

  • reload is a Boolean flag. If True, every imported module or its mock is reloaded on import. Depending on how they are written, this may help reset mocked modules from previous tests.

  • debug is a Boolean flag. If set to True, the package will produce extra prints in an attempt to help with its usage.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

test_imports-1.0.2.tar.gz (17.1 kB view hashes)

Uploaded Source

Built Distribution

test_imports-1.0.2-py3-none-any.whl (13.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page