=======
Testing
=======
Tet applications and the Tet framework itself are tested with `pytest
`_. This chapter documents the test layout actually
used in this project (Tet 0.5.0, Python 3.8+), the fixtures that ship in
``tests/conftest.py``, and practical patterns for testing your own Tet
applications.
Running the Tests
=================
Install Tet together with its test dependencies in editable mode and run
``pytest``::
# Test dependencies only (pytest, pytest-cov)
pip install -e '.[test]'
# Full development toolchain (pytest, pytest-cov, black, ruff, mypy)
pip install -e '.[dev]'
# Run the whole suite
pytest
The project configures pytest in ``pyproject.toml`` under
``[tool.pytest.ini_options]``. The relevant settings are::
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
addopts = "-ra -q --strict-markers"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
]
Because ``testpaths`` is set to ``tests``, a bare ``pytest`` invocation
collects only the ``tests/`` directory. ``--strict-markers`` means every
marker must be declared in ``markers`` (above) or collection fails, so use the
declared markers rather than inventing new ones.
Two markers are available:
``slow``
Mark long-running tests. Deselect them with ``pytest -m "not slow"``.
``integration``
Mark integration tests. Run only these with ``pytest -m integration``.
.. code-block:: python
import pytest
@pytest.mark.slow
def test_expensive_operation(): ...
@pytest.mark.integration
def test_full_request_cycle(app): ...
Coverage is available through ``pytest-cov`` (installed by both the ``test``
and ``dev`` extras)::
pytest --cov=tet --cov-report=term-missing
Built-in Fixtures
=================
The shared ``tests/conftest.py`` provides a small set of fixtures used
throughout the suite. They are all function-scoped.
``pyramid_config``
A real :class:`pyramid.config.Configurator` with ``config.begin()`` already
called; ``config.end()`` runs automatically on teardown. Use it to test
``includeme`` functions and configuration directives.
``pyramid_request``
A :class:`pyramid.testing.DummyRequest` whose ``registry`` attribute is a
:class:`unittest.mock.Mock`.
``pyramid_request_with_json``
Like ``pyramid_request`` but with ``request.json_body`` set to an empty
``dict``.
``mock_db_session``
A :class:`unittest.mock.Mock` with ``query``, ``add``, ``commit``,
``rollback`` and ``flush`` attributes pre-created as mocks.
``mock_model``
A :class:`unittest.mock.Mock` with ``__tablename__`` set to
``"test_model"``.
The actual definitions look like this:
.. code-block:: python
# tests/conftest.py
from unittest.mock import Mock
import pytest
from pyramid import testing
from pyramid.config import Configurator
@pytest.fixture
def pyramid_config():
config = Configurator()
config.begin()
yield config
config.end()
@pytest.fixture
def pyramid_request():
request = testing.DummyRequest()
request.registry = Mock()
return request
@pytest.fixture
def mock_db_session():
session = Mock()
session.query = Mock()
session.add = Mock()
session.commit = Mock()
session.rollback = Mock()
session.flush = Mock()
return session
Testing ``includeme`` Functions
===============================
Most Tet modules expose an ``includeme(config)`` entry point. The
``pyramid_config`` fixture makes these easy to exercise. For example, the CSRF
module sets ``require_csrf=True``:
.. code-block:: python
# tests/test_security_csrf.py
from unittest.mock import Mock
from tet.security.csrf import includeme
def test_includeme_sets_csrf_defaults(pyramid_config):
pyramid_config.set_default_csrf_options = Mock()
includeme(pyramid_config)
pyramid_config.set_default_csrf_options.assert_called_once_with(require_csrf=True)
Testing Views
=============
Pyramid views can be called directly with a dummy request. Use the
``pyramid_request`` fixture rather than constructing a request by hand:
.. code-block:: python
# tests/test_views.py
from myapp.views import home_view
def test_home_view(pyramid_request):
response = home_view(pyramid_request)
assert response["message"] == "Hello, World!"
When a view reads JSON from the request body, use
``pyramid_request_with_json`` and set the body content you need:
.. code-block:: python
def test_api_view(pyramid_request_with_json):
pyramid_request_with_json.json_body = {"name": "example"}
from myapp.views import create_view
result = create_view(pyramid_request_with_json)
assert result["created"] is True
Testing the JSON Renderer
=========================
Tet's JSON renderer lives in :mod:`tet.renderers.json`. The public surface is:
``construct_default_renderer(renderer_factory=JSON, **renderer_args)``
Builds a Pyramid :class:`pyramid.renderers.JSON` renderer pre-loaded with
adapters for :class:`datetime.datetime`, :class:`datetime.date`, and (when
SQLAlchemy is installed) SQLAlchemy keyed tuples.
``hook_json_renderer(config, *, renderer, name="json")``
Registers a renderer under a name and records it in the per-registry
renderer registry.
``add_json_adapter(config, *, for_, adapter, renderer="json")``
Adds a type adapter to a named, already-registered renderer.
``includeme(config)``
Registers the default renderer and adds the ``add_json_renderer`` and
``add_json_adapter`` directives.
Note that ``construct_default_renderer`` returns a Pyramid ``JSON`` renderer
*factory* instance. It is not a plain callable that turns data into a string;
to actually render, Pyramid calls it with renderer ``info`` to obtain the
render function. The simplest way to test serialization is therefore to test
the adapters and helpers directly, or to register the renderer on a
configurator. To check that the default adapters are present:
.. code-block:: python
# tests/test_renderers_json.py
from tet.renderers.json import construct_default_renderer
def test_default_renderer_constructs():
renderer = construct_default_renderer()
# It is a Pyramid JSON renderer factory instance with adapters added.
assert renderer is not None
To test the configuration directives, use ``pyramid_config`` and inspect the
per-registry renderer registry that ``hook_json_renderer`` maintains:
.. code-block:: python
from unittest.mock import Mock
from tet.renderers.json import hook_json_renderer
def test_hook_json_renderer_default_name(pyramid_config):
renderer = Mock()
pyramid_config.add_renderer = Mock()
pyramid_config.registry.tet_json_renderers = {}
hook_json_renderer(pyramid_config, renderer=renderer)
pyramid_config.add_renderer.assert_called_once_with("json", renderer)
assert pyramid_config.registry.tet_json_renderers["json"] is renderer
Testing Safe JSON Serialization
===============================
:func:`tet.util.json.js_safe_dumps` escapes characters that are dangerous
inside inline ``