Skip to content

Host API

The four functions a host project's main.py and modules/__init__.py typically reach for. setup() and register_modules() are wrappers that hide the low-level details and are sufficient for most projects; activate() and protect() are the primitives behind them and are useful when you need finer control.

setup

setup

setup(*, env_var: str = 'VIUR_TESTING', namespace: str | None = _UNSET, database: str = DEFAULT_DATABASE, api_dir: str | None = 'testing') -> None

One-call host-side wiring for main.py.

Must be the first line of code in main.py — before any from viur.core ... import. Internally:

  1. Resolves enabled + namespace. Without an explicit namespace (the usual case) it parses os.environ[env_var] (default VIUR_TESTING) via :func:viur.testing.mode.parse_spec. An explicit namespace kwarg forces test mode on and skips the env var.
  2. When enabled, calls :func:activate (datastore client swap to database + namespace, probe, validator) and loads the project test API.
  3. Calls :func:protect unconditionally to install the production cookie guard.

The single env var carries on/off + an optional namespace::

$ VIUR_TESTING=1     # on, default namespace
$ VIUR_TESTING=ak    # on, namespace "ak"
# unset / 0 / off / false → off

There is no separator and no reserved names — VIUR_TESTING=ak means "on, namespace ak". See :func:viur.testing.mode.parse_spec.

Use the env var, not conf.instance.is_dev_server, as the gate — reading conf.instance triggers the full viur.core import chain (including viur.core.db.transport), which would leave :func:activate with no chance to patch the singleton.

Typical host wiring::

# main.py
import viur.testing
viur.testing.setup()

from viur.core import setup as core_setup
import modules, render
app = core_setup(modules, render)

Parameters:

Name Type Description Default
env_var str

Name of the single variable to read. Default VIUR_TESTING.

'VIUR_TESTING'
namespace str | None

Explicit Datastore namespace override. When given, test mode is forced on and the env var is ignored; None or the empty string select the default namespace. When several testers share one viur-tests database, giving each their own namespace keeps their entities from colliding. Omit the argument to read the env var instead.

_UNSET
database str

Name of the test database to swap to. Default viur-tests.

DEFAULT_DATABASE
api_dir str | None

Name of the wrapper directory (relative to the caller's parent dir) that contains an api/ subfolder with the project test API package. setup() loads <api_dir>/api/__init__.py and registers it as the top-level Python package api via importlib — no sys.path manipulation, no sibling-directory exposure.

Default: "testing" — resolves to <dirname(main.py)>/../testing/api/ and matches the convention testing/api/ (backend fixtures) + testing/e2e/ (Playwright). Pass any other string to relocate the wrapper, or None to skip the project test API entirely.

If the resolved __init__.py does not exist, a one-line info message is printed and setup continues — that helps spot misconfigurations early (you'd otherwise see mysterious 404 /_test/<spec>/setup errors from the runner side).

'testing'
Source code in src/viur/testing/__init__.py
def setup(
    *,
    env_var: str = "VIUR_TESTING",
    namespace: "str | None" = _UNSET,
    database: str = DEFAULT_DATABASE,
    api_dir: str | None = "testing",
) -> None:
    """One-call host-side wiring for ``main.py``.

    Must be the **first** line of code in ``main.py`` — before any
    ``from viur.core ...`` import. Internally:

    1. Resolves *enabled* + *namespace*. Without an explicit ``namespace``
       (the usual case) it parses ``os.environ[env_var]`` (default
       ``VIUR_TESTING``) via :func:`viur.testing.mode.parse_spec`. An
       explicit ``namespace`` kwarg forces test mode on and skips the env var.
    2. When enabled, calls :func:`activate` (datastore client swap to
       ``database`` + ``namespace``, probe, validator) and loads the project
       test API.
    3. Calls :func:`protect` unconditionally to install the production
       cookie guard.

    The single env var carries on/off + an optional namespace::

        $ VIUR_TESTING=1     # on, default namespace
        $ VIUR_TESTING=ak    # on, namespace "ak"
        # unset / 0 / off / false → off

    There is no separator and no reserved names — ``VIUR_TESTING=ak`` means
    "on, namespace ak". See :func:`viur.testing.mode.parse_spec`.

    Use the env var, **not** ``conf.instance.is_dev_server``, as the
    gate — reading ``conf.instance`` triggers the full ``viur.core``
    import chain (including ``viur.core.db.transport``), which would
    leave :func:`activate` with no chance to patch the singleton.

    Typical host wiring::

        # main.py
        import viur.testing
        viur.testing.setup()

        from viur.core import setup as core_setup
        import modules, render
        app = core_setup(modules, render)

    :param env_var: Name of the single variable to read. Default
        ``VIUR_TESTING``.
    :param namespace: Explicit Datastore namespace override. When given,
        test mode is forced on and the env var is ignored; ``None`` or the
        empty string select the default namespace. When several testers
        share one ``viur-tests`` database, giving each their own namespace
        keeps their entities from colliding. Omit the argument to read the
        env var instead.
    :param database: Name of the test database to swap to. Default
        ``viur-tests``.
    :param api_dir: Name of the wrapper directory (relative to the
        caller's parent dir) that contains an ``api/`` subfolder
        with the project test API package. ``setup()`` loads
        ``<api_dir>/api/__init__.py`` and registers it as the
        top-level Python package ``api`` via ``importlib`` — no
        ``sys.path`` manipulation, no sibling-directory exposure.

        Default: ``"testing"`` — resolves to
        ``<dirname(main.py)>/../testing/api/`` and matches the
        convention ``testing/api/`` (backend fixtures) +
        ``testing/e2e/`` (Playwright). Pass any other string to
        relocate the wrapper, or ``None`` to skip the project
        test API entirely.

        If the resolved ``__init__.py`` does not exist, a one-line
        info message is printed and setup continues — that helps
        spot misconfigurations early (you'd otherwise see mysterious
        ``404 /_test/<spec>/setup`` errors from the runner side).
    """
    if namespace is _UNSET:
        enabled, ns = parse_spec(_os.environ.get(env_var))
    else:
        enabled = True
        ns = None if (namespace is None or namespace == "") else namespace

    if enabled:
        activate(database=database, namespace=ns)
        if api_dir is not None:
            _load_project_api(api_dir)
    protect()

register_modules

register_modules

register_modules(target: dict) -> None

Inject :class:~viur.testing._test.TestModule into the host's modules/ namespace if test mode is active.

Call from modules/__init__.py after the auto-discovery loop — typically::

# modules/__init__.py
from viur.testing import register_modules
register_modules(globals())

Idempotent: if :func:activate has not run (test mode not armed) this is a no-op, so the same line stays in place for both dev and production deployments.

TestModule is registered as a class, not an instance, so viur-core's __build_app (which scans vars(modules) for Module subclasses) picks it up and routes /_test/config/* through it.

Parameters:

Name Type Description Default
target dict

The modules/__init__.py global namespace dict, typically globals().

required
Source code in src/viur/testing/__init__.py
def register_modules(target: dict) -> None:
    """Inject :class:`~viur.testing._test.TestModule` into the host's
    ``modules/`` namespace if test mode is active.

    Call from ``modules/__init__.py`` after the auto-discovery loop —
    typically::

        # modules/__init__.py
        from viur.testing import register_modules
        register_modules(globals())

    Idempotent: if :func:`activate` has not run (test mode not armed)
    this is a no-op, so the same line stays in place for both dev and
    production deployments.

    ``TestModule`` is registered as a **class**, not an instance, so
    viur-core's ``__build_app`` (which scans ``vars(modules)`` for
    Module subclasses) picks it up and routes ``/_test/config/*``
    through it.

    :param target: The ``modules/__init__.py`` global namespace dict,
        typically ``globals()``.
    """
    from ._test.config import ConfigModule  # noqa: PLC0415

    if not ConfigModule.is_active():
        return  # test mode not armed — nothing to mount

    from ._test import TestModule  # noqa: PLC0415

    target["_test"] = TestModule

activate

activate

activate(*, database: str = DEFAULT_DATABASE, namespace: str | None = None) -> None

Switch the running process into test mode.

Must be called before any viur.core import. Performs:

  1. viur.core.db.transport not-yet-imported precondition check.
  2. conf.instance.is_dev_server precondition check.
  3. Construction of a datastore client targeting database (and namespace, if given).
  4. Synchronous probe roundtrip in that database/namespace.
  5. Patching of viur.core.db.transport.__client__.
  6. Patching of viur.core.db.types.Key.__init__ to default database= and namespace= to the client's values — without this every Key viur-core constructs goes to the wrong database/ namespace and Datastore rejects the request (or, worse, silently returns empty results).
  7. Patching of google.cloud.datastore.Key.to_legacy_urlsafe so it tolerates named databases — the original raises on them and viur-core's str(key) cascade goes through it on hot paths (session save, JSON render of login_success). See :func:_patch_legacy_urlsafe.
  8. Activation of the in-process state on :class:~viur.testing._test.config.ConfigModule.
  9. Installation of the request validator.
  10. Wrap of viur.core.setup so the dev-server boot banner gains a database = <name> (and, when set, namespace = <name>) line — see :mod:viur.testing.banner.

No token is created here — the session token is created and stored by /_test/config/status directly in the test database, and released by /_test/config/finish.

Parameters:

Name Type Description Default
database str

Name of the target test database. Default viur-tests.

DEFAULT_DATABASE
namespace str | None

Optional Datastore namespace to scope every read and write to. When several testers share one viur-tests database, giving each their own namespace (e.g. ak, mb, ci-pr-42) keeps their entities from colliding without needing to provision separate databases. An empty string is normalised to None — same convention as the VIUR_TESTING namespace part in :func:viur.testing.setup, so direct programmatic activation and env-var-driven boot behave identically.

None

Raises:

Type Description
RuntimeError

if any of the precondition checks or the probe fail. The process must abort rather than continue with a half-applied swap.

Source code in src/viur/testing/activation.py
def activate(*, database: str = DEFAULT_DATABASE, namespace: str | None = None) -> None:
    """Switch the running process into test mode.

    Must be called before any ``viur.core`` import. Performs:

    1. ``viur.core.db.transport`` not-yet-imported precondition check.
    2. ``conf.instance.is_dev_server`` precondition check.
    3. Construction of a datastore client targeting ``database`` (and
       ``namespace``, if given).
    4. Synchronous probe roundtrip in that database/namespace.
    5. Patching of ``viur.core.db.transport.__client__``.
    6. Patching of ``viur.core.db.types.Key.__init__`` to default
       ``database=`` and ``namespace=`` to the client's values — without
       this every Key viur-core constructs goes to the wrong database/
       namespace and Datastore rejects the request (or, worse, silently
       returns empty results).
    7. Patching of ``google.cloud.datastore.Key.to_legacy_urlsafe`` so
       it tolerates named databases — the original raises on them and
       viur-core's ``str(key)`` cascade goes through it on hot paths
       (session save, JSON render of login_success).
       See :func:`_patch_legacy_urlsafe`.
    8. Activation of the in-process state on
       :class:`~viur.testing._test.config.ConfigModule`.
    9. Installation of the request validator.
    10. Wrap of ``viur.core.setup`` so the dev-server boot banner gains
       a ``database = <name>`` (and, when set, ``namespace = <name>``)
       line — see :mod:`viur.testing.banner`.

    No token is created here — the session token is created and stored
    by ``/_test/config/status`` directly in the test database, and
    released by ``/_test/config/finish``.

    :param database: Name of the target test database. Default ``viur-tests``.
    :param namespace: Optional Datastore namespace to scope every read
        and write to. When several testers share one ``viur-tests``
        database, giving each their own namespace (e.g. ``ak``, ``mb``,
        ``ci-pr-42``) keeps their entities from colliding without
        needing to provision separate databases. An empty string is
        normalised to ``None`` — same convention as the ``VIUR_TESTING``
        namespace part in :func:`viur.testing.setup`, so direct
        programmatic activation and env-var-driven boot behave identically.
    :raises RuntimeError: if any of the precondition checks or the probe
        fail. The process must abort rather than continue with a
        half-applied swap.
    """
    # Normalise empty string → None at the public seam so every downstream
    # helper (client construction, key factory patch, ConfigModule state)
    # sees the same canonical form. Mirrors setup()'s env-var handling.
    if namespace == "":
        namespace = None

    _require_transport_not_loaded()
    _require_dev_server()

    client = _build_test_client(database, namespace=namespace)
    _probe_roundtrip(client, database)

    _patch_transport_client(client)
    _patch_key_factory(client)
    _patch_legacy_urlsafe()

    # Safe to import ConfigModule now: transport has been patched, so
    # any transitive viur-core import that touches ``__client__`` will
    # find our test client.
    from ._test.config import ConfigModule  # noqa: PLC0415
    ConfigModule.set_active(
        database=database, project_id=client.project, namespace=namespace,
    )

    _install_request_validator()
    _open_bootstrap_paths_in_closed_system()

    from .banner import install_banner_patch  # noqa: PLC0415
    install_banner_patch(database, namespace=namespace)

protect

protect

protect() -> None

Install :class:ProductionGuardValidator into the viur-core router.

Idempotent — calling twice is a no-op. Safe to call in any environment.

Source code in src/viur/testing/protection.py
def protect() -> None:
    """Install :class:`ProductionGuardValidator` into the viur-core router.

    Idempotent — calling twice is a no-op. Safe to call in any
    environment.
    """
    from viur.core.request import Router  # noqa: PLC0415
    from .validator import ProductionGuardValidator  # noqa: PLC0415

    if ProductionGuardValidator not in Router.requestValidators:
        Router.requestValidators.append(ProductionGuardValidator)