Test for import of optional dependencies in __init__.py with pytest: Python 3.5 /3.6 differs in behaviour

后端 未结 3 1475
太阳男子
太阳男子 2020-12-20 22:09

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

相关标签:
3条回答
  • 2020-12-20 22:48
    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
    
    0 讨论(0)
  • 2020-12-20 22:56

    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:

    Altering 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.

    Mocking __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.

    0 讨论(0)
  • 2020-12-20 23:06

    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 ==========================
    
    0 讨论(0)
提交回复
热议问题