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
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 matchfoo.bar
andfoo.bard
, but notfoodbar
(because dot is matched literally, not as in regular expressions). Further,"foo.*r"
will matchfoo.bar
, but notfoo.bard
because only the whole strings are matched. If you wantfoo.bard
to match, the expression needs to befoo.*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 fromsys.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 toModuleNotFoundError
. -
debug
is a Boolean flag. If set toTrue
, 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, ifprefix
is set to its default version"TI_"
, then the debugging value is assigned asTI_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 fromsys.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. IfTrue
, 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 toTrue
, the package will produce extra prints in an attempt to help with its usage.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file test_imports-1.0.2.tar.gz
.
File metadata
- Download URL: test_imports-1.0.2.tar.gz
- Upload date:
- Size: 17.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.23.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | e41a45786716455007fd468c75b0308707050f585d46d4c8f1bf369a343a0ecd |
|
MD5 | 3dc11e7f5eebd9828aef2349521c916b |
|
BLAKE2b-256 | 9d1a8bc14d4aa638dca1b3257a86cc43756e617ccd4fd567f16affc72faab0aa |
File details
Details for the file test_imports-1.0.2-py3-none-any.whl
.
File metadata
- Download URL: test_imports-1.0.2-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.23.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | cee042385fe451ad17ef06a1824991afaf03571e3bb79f497c31c9b3d1d712ac |
|
MD5 | c1a9d9d136c4e123c9d1f0188ad215cb |
|
BLAKE2b-256 | d39f26c59d877210ae3c12932c429dc58e118d97db639f78fb454af7342171e9 |