# Test
# pytest
Schema
import pytest def test_(): """ This test function can be discovered """ pass class MyClass: """ No __init__ """ def test_sth(self): """ This test function (starts with test and self as parameter) can be discovered """ pass
Command line
> pytest ocr_test.py -k "test_single_document_endpoint"
keyword expression
-k
> pytest ocr_test.py -k "123 and not 456"
Runs 123.678 not 123.456
> pytest ocr_test.py -k "123 or 456"
Runs 123 and 456
> touch repo/conftest.py repo ├── conftest.py ├── app.py ├── settings.py ├── models.py └── tests └── test_app.py
pytest automatically adds PYTHONPATH as the path where conftest.py stays
> pytest path/to/test_file.py::TestClass::test_method
Runs one test method of a class
Parameterization decorator
# content of test_expectation.py import pytest @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected
pytest_generate_tests
# content of test_strings.py def test_valid_string(stringinput): assert stringinput.isalpha()
def pytest_generate_tests(metafunc): if "stringinput" in metafunc.fixturenames: metafunc.parametrize("stringinput", some_list)
Fixture scope
Fixtures are created when first requested by a test, and are destroyed based on their scope:
function
: the default scope, the fixture is destroyed at the end of the test.class
: the fixture is destroyed during teardown of the last test in the class.module
: the fixture is destroyed during teardown of the last test in the module.package
: the fixture is destroyed during teardown of the last test in the package.session
: the fixture is destroyed at the end of the test session.
caveats: only fixture value is destroyed. internal code memory status is stable.
Fixture Availability
- PyPI projects that match “pytest-*” are considered plugins and are listed automatically. Packages classified as inactive are excluded. Full list
- Fixture availability is determined from the perspective of the test. A fixture is only available for tests to request if they are in the scope that fixture is defined in. If a fixture is defined inside a class, it can only be requested by tests inside that class. But if a fixture is defined inside the global scope of the module, than every test in that module, even if it’s defined inside a class, can request it.
Similarly, a test can also only be affected by an autouse fixture if that test is in the same scope that autouse fixture is defined in (see Autouse fixtures are executed first within their scope).
A fixture can also request any other fixture, no matter where it’s defined, so long as the test requesting them can see all fixtures involved.
class Apple: def __init__(self, count): self.value = count class SS: instance = None def __init__(self): pass @classmethod def create(cls, count): if cls.instance is None: cls.instance = Apple(count) else: print(cls.instance) cls.instance = Apple(10) return cls.instance @pytest.fixture() # default teardown after each test function def sston(): guagua = SS.create(20) return guagua class TestSS: def test_one(self, sston): assert sston.value == 20 def test_two(self, sston): assert sston.value == 10 # SS.instance is still there
conftest.py
In a wide meaning
conftest.py
is a local per-directory plugin. Here you define directory-specific hooks and fixtures.caveats
- when testing,
conftest.py
s defined are imported and collected - fixtures defined in the
conftest.py
in the same directory win
- when testing,
Practices
6.1 mock requests
# pytest register requests_mock as requests_mock.Mocker(real_http=False, session=None) # i.e., all requests are captured @pytest.fixture def mock_a_request(requests_mock): requests_mock.post(...) requests_mock.get(...) requests_mock.register_uir('POST', 'mock://127.0.0.1', additional_matcher=callable, json=callable) def test_sth(mock_a_request): ...
6.2 MagicMock with monkeypatch
@pytest_fixture def mock_attribute(monkeypatch): mock_sth = MagicMock() mock_sth.return_value = 'sth' monkeypatch.setattr(module, 'attribute', mock_sth) def test_sth(mock_attribute): ... ############ # Alterative ############ def test_sth(): with patch.object(module, 'attribute') as mock_sth: # a MagicMock mock_sth.return_value = 'sth' ...
6.3 mock local or remote model predictions:
- mock function call
- mock tfs requests
Folder structure
__init__.py
is needed to maintain a folder structurerequest
request.getfixturevalue('name')
logging
-s
: Stops hiding std_out/std_err for successful tests--log-cli-level=INFO
: Shows logs above INFO-p no:logging
: Turns off logging plugin
pytest-aiohttp
by
pip install pytest-aiohttp
orpytest_plugins = 'aiohttp.pytest_plugin'
in rootconftest.py
async coroutines are runnable automatically
async def some_fn(): r = await a_fn() assert r
pytest-asyncio
async coroutines are runnable with
@pytest.mark.asyncio
@pytest.mark.asyncio async def some_fn(): r = await a_fn() assert r
take care of contextvars usage
import aiocontextvars class CM: def __init__(self): pass async def __aenter__(self): c = aiocontextvars._get_context() self.c = c return 10 async def __aexit__(self, exc_type, exc_val, exc_tb): c2 = aiocontextvars._get_context() assert self.c == c2 # True assert self.c is c2 # False as pytest_asyncio handles aenter, aexit in two successive contexts, and the later is a copy of the former @pytest.fixture async def some_fn_with_context_manager(): async with CM(): yield async def test_debug(some_fn_with_context_manager): pass # tear_down fails # from https://github.com/pytest-dev/pytest-asyncio/issues/127 from contextvars import ContextVar import pytest blah = ContextVar("blah") @pytest.fixture async def my_context_var(): blah.set("hello") assert blah.get() == "hello" yield blah @pytest.mark.asyncio async def test_blah(my_context_var): assert my_context_var.get() == "hello" # this fails, The cause appears to be that the async fixtures are ran in own Tasks.
celery
def test_some_task(celery_worker): some_task.delay() # if no celery worker, some_task hangs
parallel
https://www.jetbrains.com/help/pycharm/performing-tests.html#test-mutliprocessing
configuration
Many pytest settings can be set in a configuration file, which by convention resides on the root of your repository or in your tests folder.
example pytest.ini
[pytest] log_cli=true log_level=DEBUG
Issue with
ContextVars
As discussed in github,ContextVars
set beforeyield
in pytest's fixture is quaranntined from real test code
# unittest
unittest.mock wraps the outdated mock module since python 3.3
# patch
patch the correct item
If the function is used directly, instead of patching the original place where function is defined, patch the place where function is used.
If an attribute of an object is used, patching the place where the object is defined is okay (import is always the 1st step. patching before usage is important.).
"""
myapp.py
"""
from where.method.is_defined import standalone_method
from where.anotherMethod.is_defined import ObjectContainingThisMethod
class MyApp:
def __init__(self):
pass
def abc(self):
standalone_method() # method is called directly
def efg(self):
obj = ObjectContainingThisMethod()
obj.method() # method is a class attribute
"""
patch the correct item
"""
import where.method.is_used.MyApp
def test_abc():
from where.method import is_used
with patch.object(is_used, 'standalone_method'):
myapp = MyApp
myapp.abc() # method is changed to mocked
def test_efg():
from where.anotherMethod.is_defined import ObjectContainingThisMethod
with patch.object(ObjectContainingThisMethod, 'method'):
myapp = MyApp
myapp.efg() # method is changed to mocked
patch
, patch.dict
, or patch.object
can act as a function decorator, class decorator or a context manager. When used as a class decorator, methods starting with patch.TEST_PREFIX are mocked.
patch(target, new=DEFAULT, **kwargs)
- if
new
is omitted,AsyncMock
is generated for async target andMagicMock
is generated for others target
must be string with the formatpackage.module.ClassName
- can only mock one module at that path, if the path is correct
patch.object(target, attribute, new=DEFAULT, **kwargs)
- if
new
is omitted,AsyncMock
is generated for async target andMagicMock
is generated for others - can only mock one module at that path, if the path is correct
- target must be imported in unit test for
patch.object
to work target
is python variable andattribute
is string
patch.dict(in_dict, values=(), clear=False, **kwargs)
- modifies the dictionary everywhere
in_dict
can be a string('package.module.dict') or python dict variablevalues
can be a dictionary or a iterable of(key, value)
pairs- if
clear
is true,in_dict
is replaced. Otherwise,in_dict
is updated - since python 3.8, if used as context manager the mocked dict is returned
in_dict
and the mocked dict(since 3.8) are restored back after test
# affects dictionary in moduleA, moduleB, ...
# (target, value, clear)
# clear: whether to empty the dictionary before overriding
# target: imported or string
with mock.patch.dict('moduleA.dictionary', {'key':'value'}, clear=True):
pass
with mock.patch.dict(moduleA.dictionary, {'key':'value'}, clear=True):
pass
# only affects moduleA
# (target, attribute, value)
# read_only attribute cannot be patched
with mock.patch.object(moduleA, 'dictionary', {'key':'value'}):
pass
# only affects moduleA
# (target, value)
# target can be string or imported
with mock.patch('moduleA.dictionary', {'key':'value'}):
pass
# nested mock
with mock.patch('moduleA.dictionary', {'key':'value'}) as mock_a, mock.patch('moduleB.dictionary', {'key':'value'}) as mock_b:
pass
# patch.dict can also be used to mock environment variables
with mock.patch.dict('os.environ', {'hello': 'world'}, clear=True):
pass
# make patch a fixture
@pytest.fixture(autouse=True)
def mock_settings_env_vars():
with mock.patch.dict(os.environ, {"FROBNICATION_COLOUR": "ROUGE"}):
yield
side_effect
side_effect
: assign an exception, iterable, or function.
- iterable: return in order
- function: pass in the arguments from original function
handle class mock
instance method with self
needed a argument place, whereas class method with cls
does not
# Coverage
Only importable files (ones at the root of the tree, or in directories with a init.py file) will be considered.
# command
pytest --cov-report term --cov=apple --cov=orange apple_tests
"""
---------- coverage: python ----------
Name Stmts Miss Cover
---------------------------------------------------------------
apple/aaaaaaaaaa.py 41 9 78%
apple/bbbbbbbbbbbbbbb.py 48 13 73%
---------------------------------------------------------------
TOTAL 89 22 75%
"""
or
[run]
source = apple,orange
python --cov-config=.coveragerc --cov-report term --cov apple_tests
← Neural Networks PDF →