Skip to main content
A test that calls a remote function directly never constructs a full 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.
signed_in defaults to uid="u_test1" when the test doesn’t care which caller it is, only that someone is signed in.

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
Walking through what happens:
  1. signed_in("u_alice", ...) opens the outer scope. Alice’s create_comment call gets attributed to whatever current_uid() and current_extras() resolve to right then, her uid and her email.
  2. acting_as("u_bob", ...) swaps the identity without leaving the outer block. current_uid() now resolves to u_bob, and current_extras() returns only bob’s extras, alice’s never leak into the swapped block. Bob’s comment gets bob’s uid and name.
  3. Exiting the acting_as block restores alice’s identity exactly, including the cached resolution. The get_comments call right after runs back in alice’s scope, proving the swap was fully reverted.
  4. 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.
This proves the comment’s authorship comes from the resolved identity, never from client input. Nothing passed to 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:
The unknown-id case and the wrong-owner case raise the identical error, so nothing a caller observes distinguishes “doesn’t exist” from “not yours”.

Two accessors named current_uid

Fymo has two functions called current_uid, in different namespaces, answering different questions:
  • fymo.auth.current_uid() is the resolved identity from your @identify chain. This is what you attribute writes with.
  • fymo.remote.current_uid() is the anonymous fymo_uid device 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

Neither signed_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
This is adapted from Fymo’s own test suite (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.
init_providers raises FileNotFoundError when project_root has no fymo.yml. Point it at the same directory FymoApp would run from, not an empty scratch directory.

Anonymous requests

There’s no signed_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.

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.