Skip to content

Testing

  • Start here: Call recorder
  • Common recipes: record(backend) instruments a backend and returns a CallLog. Assert with log.count(method, **kwargs). FakeVerifier(alice=fake_claims("alice", "orders:read")) stands in for a JWTVerifier in an authenticated app.

grelmicro.testing

Test helpers for protocol-level interactions and authenticated apps.

record(backend) instruments a backend's public async methods in place and returns a CallLog. The backend keeps its real type and behavior, so it drops into a component exactly as before, while the log captures every call for assertions, in the spirit of pytest-mock's mocker.spy.

from grelmicro.coordination.memory import MemoryLockAdapter
from grelmicro.testing import record

backend = MemoryLockAdapter()
log = record(backend)
micro = Grelmicro(uses=[Coordination(lock=backend)])

async with micro:
    await login("u1")

assert log.count("acquire", name="user:u1") == 1

FakeVerifier stands in for a JWTVerifier: each token is a name for the claims it carries, built with fake_claims, so no key is generated and no token is signed.

from grelmicro.http import AuthenticatedRequests
from grelmicro.testing import FakeVerifier, fake_claims

verifier = FakeVerifier(alice=fake_claims("alice", "orders:read"))
micro = Grelmicro(uses=[AuthenticatedRequests(verifier)])

CallLog dataclass

CallLog(calls: list[Call] = list())

Records calls made to an instrumented backend.

Returned by record(...). Exposes the raw calls list plus helpers to assert on what was called.

calls class-attribute instance-attribute

calls: list[Call] = field(default_factory=list)

Every recorded call, in order.

count

count(
    method: str | None = None,
    args: tuple | None = None,
    **kwargs: Any,
) -> int

Return how many recorded calls match method, args, and kwargs.

A call matches when its method equals method (when given), its positional arguments equal args (when given), and every item in kwargs equals the recorded keyword argument of the same name.

PARAMETER DESCRIPTION
method

Method name to match, or None to count every call.

TYPE: str | None DEFAULT: None

args

Positional arguments to match, or None to skip the check.

TYPE: tuple | None DEFAULT: None

methods

methods() -> list[str]

Return the method names of every recorded call, in order.

reset

reset() -> None

Drop every recorded call.

Call dataclass

Call(
    method: str,
    args: tuple = tuple(),
    kwargs: Mapping[str, Any] = dict(),
)

One recorded protocol call.

method instance-attribute

method: str

Name of the method that was called.

args class-attribute instance-attribute

args: tuple = field(default_factory=tuple)

Positional arguments the method was called with.

kwargs class-attribute instance-attribute

kwargs: Mapping[str, Any] = field(default_factory=dict)

Keyword arguments the method was called with.

FakeVerifier

FakeVerifier(
    tokens: Mapping[str, JWTClaims] | None = None,
    /,
    **named: JWTClaims,
)

A verifier for tests: each token is a name for the claims it carries.

It answers verify and verify_header like JWTVerifier, so it drops into AuthenticatedRequests. A token it does not know is refused as invalid, a header with no bearer token as scheme, an expired token as expired, and one not valid yet as not-yet-valid.

verifier = FakeVerifier(
    alice=fake_claims("alice", "orders:read"),
    bob=fake_claims("bob"),
)
verifier.verify_header("Bearer alice").subject  # "alice"

Map each token to the claims it stands for.

PARAMETER DESCRIPTION
tokens

Tokens mapped to their claims, for a token that is not a name.

TYPE: Mapping[str, JWTClaims] | None DEFAULT: None

**named

Tokens given as keyword arguments.

TYPE: JWTClaims DEFAULT: {}

RAISES DESCRIPTION
TypeError

If a token maps to anything but JWTClaims, such as a mapping passed by keyword as tokens=.

verify

verify(token: str) -> JWTClaims

Return the claims of token, or raise TokenRejectedError.

The times are checked as JWTVerifier checks them with no leeway. exp and nbf are rounded to the nearest second and compared with the current second: a token is expired once its exp is before it, and not-yet-valid while its nbf is after it. An iat after the current time is not-yet-valid too.

PARAMETER DESCRIPTION
token

The token, with no scheme prefix.

TYPE: str

verify_header

verify_header(header: str | None) -> JWTClaims

Return the claims of the bearer token in header.

PARAMETER DESCRIPTION
header

The Authorization header value, or None.

TYPE: str | None

record

record(backend: object) -> CallLog

Instrument backend's public async methods and return their CallLog.

Each public coroutine method (one whose name does not start with _) is replaced on the instance with a wrapper that records the call and forwards to the original. The class and other instances are untouched.

PARAMETER DESCRIPTION
backend

The backend instance to instrument. Its public async methods are wrapped in place, so the same instance keeps its type and behavior.

TYPE: object

fake_claims

fake_claims(
    subject: str,
    *scopes: str,
    issuer: str | None = None,
    audience: str | Sequence[str] | None = None,
    expires_at: float = _YEAR_2100,
    issued_at: float | None = None,
    token_id: str | None = None,
    not_before: float | None = None,
    **claims: object,
) -> JWTClaims

Build the claims of a verified token, for a test.

exp defaults to 2100-01-01, so the claims never expire in a test, and the other registered claims left out stay unset. claims carries every claim given, as a token would, read-only like a verified token's.

PARAMETER DESCRIPTION
subject

The sub claim, who the caller is.

TYPE: str

*scopes

The scopes the caller holds.

TYPE: str DEFAULT: ()

issuer

The iss claim.

TYPE: str | None DEFAULT: None

audience

The aud claim.

TYPE: str | Sequence[str] | None DEFAULT: None

expires_at

The exp claim, in seconds. A fraction is rounded down. A verified token always carries one, so it defaults to 2100-01-01, which no test outlives.

TYPE: float DEFAULT: _YEAR_2100

issued_at

The iat claim, in seconds. A fraction is rounded down.

TYPE: float | None DEFAULT: None

token_id

The jti claim.

TYPE: str | None DEFAULT: None

not_before

The nbf claim, in seconds. The token is refused before it.

TYPE: float | None DEFAULT: None

**claims

Any other claim, such as tenant.

TYPE: object DEFAULT: {}

RAISES DESCRIPTION
TypeError

If the subject, a scope, the issuer, the audience or the token id is not a string, a time is not a number or expires_at is None, an extra claim is a registered one such as scope or exp, or an extra claim is not a JSON value: a string, number, boolean, None, list or dict.

ValueError

If subject is empty, a scope is empty or holds whitespace, or a time is negative, not finite or past 2**64 seconds, since a verified token never carries any of them.