FymoApp, so the process-wide seams a real request depends on are simply missing. The @identify resolver chain has nothing registered, and the storage, jobs, and broadcasts provider singletons were never built. fymo.testing exists to stand those seams up for the length of a with block, then restore everything exactly as it found it.
This page is about testing an app you build on Fymo, calling your own remote functions directly and asserting on the result. If you’re looking to run Fymo’s own test suite as a contributor to the framework itself, see Testing instead.
Why tests need help
FymoApp.__init__ does two things at startup that a bare test process skips entirely. It discovers every @identify resolver under app/auth/ and chains them together, and it builds the storage, jobs, and broadcasts providers from fymo.yml. Import a remote function in a test file and call it, and neither of those has happened.
signed_in and acting_as register a resolver through the exact same @identify mechanism a live request uses, so there’s no separate mock path that can drift out of sync with production behavior. init_providers builds providers the same way FymoApp.__init__ does. All three restore the prior state on exit, so one test’s setup never leaks into the next.
The examples below use two small helpers from the reference blog app: a
db fixture (an isolated SQLite file per test, see examples/blog_app/tests/conftest.py) and _seed_post, which inserts one post row directly. Neither is part of fymo.testing, they’re just how this particular app sets up test data.Signing in a caller
signed_in(uid, *, extras=None) is a context manager. It registers a resolver that resolves to Identity(uid=uid), opens a request scope, and yields the Identity. Inside the block, current_uid() returns uid, and a remote function guarded with @require_auth runs normally instead of raising.
Here’s the simplest case, a single signed-in call, adapted from the reference blog app’s test suite:
tests/test_comments.py
create_comment is called directly, no WSGI request and no test client involved. Inside the block, current_uid() resolves to u_alice, and current_extras() (the typed accessor app/auth/extras.py generates) reads back the extras passed in.
extras stands in for whatever your app’s identity extras hook would normally attach on a real request, an email, a role, whatever your register_identity_extras_hook callback returns. Pass it whenever the code under test reads identity_extras() or a typed accessor built on it.
Proving isolation
acting_as(uid, *, extras=None) swaps in a second identity mid-block. It must be nested inside signed_in(), calling it on its own raises RuntimeError telling you to wrap the body in a signed_in() block first. This is the tool for proving one identity can’t touch another’s data.
Here’s the real test from the reference blog app, python -m pytest tests/test_comments.py -v passes it as written:
tests/test_comments.py
signed_in("u_alice", ...)opens the outer scope. Alice’screate_commentcall gets attributed to whatevercurrent_uid()andcurrent_extras()resolve to right then, her uid and her email.acting_as("u_bob", ...)swaps the identity without leaving the outer block.current_uid()now resolves tou_bob, andcurrent_extras()returns only bob’s extras, alice’s never leak into the swapped block. Bob’s comment gets bob’s uid and name.- Exiting the
acting_asblock restores alice’s identity exactly, including the cached resolution. Theget_commentscall right after runs back in alice’s scope, proving the swap was fully reverted. - Both comments exist by the end. The set of names is
{"alice", "bob"}, and a direct row check confirms the two rows carry two different uids.
create_comment says who alice or bob is. The name comes out of current_extras(), which comes out of whatever identity fymo resolved for that scope, and there’s no argument a caller could pass to write a different uid into that row.
create_comment reads the author from current_extras(), splitting the email at @ for a display name. That’s why both signed_in and acting_as pass matching extras here, without them the author would fall back to the raw uid instead of a name.Ownership answers NotFound
The other half of the isolation story is what a second user gets when they try to touch a row they don’t own:NotFound, not Forbidden. A 403 would confirm the row exists to someone who shouldn’t know that. This is the convention fymo generate resource bakes into its generated code and its generated tests, and it’s worth asserting in your own:
Two accessors named current_uid
Fymo has two functions calledcurrent_uid, in different namespaces, answering different questions:
fymo.auth.current_uid()is the resolved identity from your@identifychain. This is what you attribute writes with.fymo.remote.current_uid()is the anonymousfymo_uiddevice cookie every browser gets, signed in or not. It’s for things like deduping reactions, never for authorship.
signed_in deliberately opens its request scope with a device uid distinct from the identity uid (u_device_<uid> versus <uid>), because in a real browser the two never coincide. Code that attributes a write through the wrong accessor fails its tests here instead of failing in production, which is exactly the kind of bug this separation exists to catch.
Bootstrapping providers
Neithersigned_in nor acting_as touches storage, jobs, or broadcasts. A bare test process still has no providers installed, so calls like get_storage_provider() fail even inside a signed_in block. init_providers(project_root) fixes that by reading the project’s real fymo.yml and building the same providers FymoApp.__init__ would, for the length of the block.
tests/test_upload.py
tests/testing/test_init_providers.py), which passes as written. init_providers also yields a namespace directly, so with init_providers(project) as providers: gives you providers.storage, providers.jobs, and providers.broadcasts already built, an alternative to calling the getters yourself.
Storage stays None when fymo.yml has no storage: section, matching FymoApp exactly: there’s no default provider. Jobs and broadcasts are always initialized. On exit, every provider singleton is restored to whatever it was before the block.
Anonymous requests
There’s nosigned_out or as_anonymous helper, because there’s nothing to simulate. Open a plain request scope with no signed_in wrapper, and current_uid() returns None exactly as it does for a real anonymous visitor:
request_scope is the same primitive signed_in builds on, it just never registers a resolver. With the @identify chain empty, current_uid() resolves to None, the same outcome any unrecognized caller gets on a real request.
Related pages
Authentication
The identity model signed_in and acting_as exercise: resolvers, current_uid(), and identity extras.
Testing
Running Fymo’s own test suite, for contributing to the framework rather than testing an app built on it.
