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.barandfoo.bard, but notfoodbar(because dot is matched literally, not as in regular expressions). Further,"foo.*r"will matchfoo.bar, but notfoo.bardbecause only the whole strings are matched. If you wantfoo.bardto 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_modulesis 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. -
exceptionis either a class or an instance of the exception to be raised when an import fails. Unsurprisingly, this defaults toModuleNotFoundError. -
debugis 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:
-
prefixis the prefix for keyword-only arguments that are recognised by this function. For example, ifprefixis 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. -
dotis 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_modulesis 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. -
reloadis 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. -
debugis 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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
|