Testing
- Start here: Call recorder
- Common recipes:
record(backend)instruments a backend and returns aCallLog. Assert withlog.count(method, **kwargs).FakeVerifier(alice=fake_claims("alice", "orders:read"))stands in for aJWTVerifierin 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
TYPE:
|
args
|
Positional arguments to match, or
TYPE:
|
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
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:
|
**named
|
Tokens given as keyword arguments.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If a token maps to anything but |
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:
|
verify_header
verify_header(header: str | None) -> JWTClaims
Return the claims of the bearer token in header.
| PARAMETER | DESCRIPTION |
|---|---|
header
|
The
TYPE:
|
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:
|
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
TYPE:
|
*scopes
|
The scopes the caller holds.
TYPE:
|
issuer
|
The
TYPE:
|
audience
|
The
TYPE:
|
expires_at
|
The
TYPE:
|
issued_at
|
The
TYPE:
|
token_id
|
The
TYPE:
|
not_before
|
The
TYPE:
|
**claims
|
Any other claim, such as
TYPE:
|
| 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
|
ValueError
|
If |