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_pathAdd 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 ==========================