tests/ and mirrors the shape of the fymo/ package. Each top-level module gets its own test directory, plus one extra directory for tests that cross module boundaries. As of this writing, the suite has 108 test_*.py files.
Two example apps live in the examples folder and double as test fixtures: a simple todo_app and a fuller blog_app. blog_app has real app/remote/*.py functions with auth turned on. That’s why integration tests reach for it to exercise remote-function discovery, SSR, or a full request/response cycle.
Directory layout
Modules with multiple providers, like broadcast and jobs, split provider-specific tests into their own
providers/ subdirectory. That keeps tests/jobs/providers/test_procrastinate.py next to tests/jobs/test_job_runner.py instead of everything living flat in one folder.
Naming and config
Pytest is configured inpyproject.toml:
test_*.py and live under tests/. Most tests are plain functions matching test_*, though Test* classes work too if you prefer them. A tests/__init__.py file lets integration tests import helpers directly, for example from tests.integration._seed_helpers import seed_test_post.
Running tests
The project usesuv, so you can run the full suite with:
.github/workflows/ci.yml.
Narrow things down when you only care about one part of the suite. Run everything in a module:
Some fixtures copy an example app, either
todo_app or blog_app, before running tests against it. If that example’s node_modules folder isn’t installed yet, the fixture skips with a clear message instead of failing. Run npm install inside the example directory first if you see one of those skips.Unit vs integration
Most of the suite is made up of unit tests: one test file per source module, checking that module’s behavior in isolation.tests/core/test_html.py is a good example of this pattern. It calls build_html() directly with a hand-built RouteAssets object and checks the markup that comes back, without spinning up a server or a build pipeline.
tests/integration/ directory is different: it runs several layers together against a real example app. A fymo build runs for real, a WSGI request goes through the actual app, and a remote call round-trips through the devalue wire format.
tests/integration/test_blog_e2e.py is the best walkthrough of this. It builds blog_app, then drives it with a small _wsgi_call() helper. That helper constructs a WSGI environ dict and collects the response, with no test client library involved.
One test in that file exercises a full user journey. It renders the index page and a post detail page, then calls the get_posts remote function. It confirms an anonymous create_comment call is rejected with a 401. Then it signs up through the generated auth client, and confirms the authenticated create_comment call succeeds, with the author taken from the session rather than from client input.
Integration tests need a real SQLite row to render against. tests/integration/_seed_helpers.py exposes a seed_test_post() helper. It inserts a post directly via app.data.db, skipping markdown parsing entirely.
This helper replaced an older markdown-file seeder. Call it after the blog_app fixture has put the example on sys.path, and before building or hitting the app.
Test fixtures
tests/conftest.py defines the fixtures shared across the suite:
example_app / blog_app
Copies an example app into an isolated
tmp_path so tests don’t share state. It works for both todo_app and blog_app. The original node_modules folder gets symlinked in rather than copied, which keeps setup fast.For blog_app, the fixture also places the copy on sys.path. Afterward it cleans up both sys.path and any cached app.* modules. That matters because every copy shares one top-level package name: app.node_available
Session-scoped. Skips a test automatically if
node isn’t available on PATH. Used by anything that needs the SSR sidecar or a real fymo build._fymo_secret_for_tests
Autouse and session-scoped. Sets
FYMO_SECRET so a FymoApp can be constructed without extra setup. Individual tests don’t need to set dev=True or manage a .fymo/secret.key file themselves._reset_remote_router_globals
Autouse.
FymoApp.__init__ writes directly onto the fymo.remote.router module. It sets two flags there, _explicit_optin and _dev_mode. This fixture resets both back to False after every test.Without it, a test that builds over blog_app, which sets remote.mode: strict (resolving to the same internal flag), could leak that setting into unrelated tests running later in the same process.A subtle bug this cleanup prevents
A subtle bug this cleanup prevents
This cleanup exists to guard against a specific bug. Without it, two tests using separate copies of
blog_app could collide.The second test would silently reuse the first test’s cached app.data.db module instead of its own. That means seed_test_post() could write into the wrong SQLite file, the one belonging to the other test.If you write a fixture that imports from app.*, follow this same cleanup pattern.