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