I have a package for python 3.5 and 3.6 that has optional dependencies for which I want tests (pytest) that run on either version.
I made a reduced example below co
import sys
from unittest.mock import patch
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
# do whatever you want
What the above code does is, it mocks that the package optional_dependency
is not installed and runs your test in that isolated environment inside the context-manager(with
).
Keep in mind, you may have to reload the module
under test depending upon your use case
import sys
from unittest.mock import patch
from importlib import reload
def test_without_dependency(self):
with patch.dict(sys.modules, {'optional_dependency': None}):
reload(sys.modules['my_module_under_test'])
# do whatever you want
I would either mock the __import__ function (the one invoked behind the import modname
statement), or customize the import mechanism by adding a custom meta path finder. Examples:
sys.meta_path
Add a custom MetaPathFinder implementation that raises an ImportError
on an attempt of importing any package in pkgnames
:
class PackageDiscarder:
def __init__(self):
self.pkgnames = []
def find_spec(self, fullname, path, target=None):
if fullname in self.pkgnames:
raise ImportError()
@pytest.fixture
def no_requests():
sys.modules.pop('requests', None)
d = PackageDiscarder()
d.pkgnames.append('requests')
sys.meta_path.insert(0, d)
yield
sys.meta_path.remove(d)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests2')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
The fixture no_requests
will alter sys.meta_path
when invoked, so the custom meta path finder filters out the requests
package name from the ones that can be imported (we can't raise on any import or pytest
itself will break). cleanup_imports
is just to ensure that mypackage
will be reimported in each test.
__import__
import builtins
import sys
import pytest
@pytest.fixture
def no_requests(monkeypatch):
import_orig = builtins.__import__
def mocked_import(name, globals, locals, fromlist, level):
if name == 'requests':
raise ImportError()
return import_orig(name, locals, fromlist, level)
monkeypatch.setattr(builtins, '__import__', mocked_import)
@pytest.fixture(autouse=True)
def cleanup_imports():
yield
sys.modules.pop('mypackage', None)
def test_requests_available():
import mypackage
assert mypackage.requests_available
@pytest.mark.usefixtures('no_requests')
def test_requests_missing():
import mypackage
assert not mypackage.requests_available
Here, the fixture no_requests
is responsible for replacing the __import__
function with one that will raise on import requests
attempt, doing fine on the rest of imports.
If a test tests optional functionality, it should be skipped rather than passed if that functionality is missing.
test.support.import_module() is the function used in the Python autotest suite to skip a test or a test file if a module is missing:
import test.support
import unittest
nonexistent = test.support.import_module("nonexistent")
class TestDummy(unittest.testCase):
def test_dummy():
self.assertTrue(nonexistent.vaporware())
Then, when running:
> python -m py.test -rs t.py
<...>
collected 0 items / 1 skipped
=========================== short test summary info ===========================
SKIP [1] C:\Python27\lib\test\support\__init__.py:90: SkipTest: No module named
nonexistent
========================== 1 skipped in 0.05 seconds ==========================