> ## Documentation Index
> Fetch the complete documentation index at: https://fymo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> How the framework's own test suite is organized, and how to run it.

Fymo's test suite lives under `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

| Directory           | Mirrors               | Covers                                                                                                                     |
| ------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `tests/auth`        | `fymo/auth`           | passwords, OAuth, sessions, Clerk provider, email verification                                                             |
| `tests/broadcast`   | `fymo/broadcast`      | SSE channels, publish/subscribe, provider registry                                                                         |
| `tests/build`       | `fymo/build`          | esbuild pipeline, manifest generation, directory hygiene checks                                                            |
| `tests/cli`         | `fymo/cli`            | `fymo init`, `fymo new`, the jobs worker command                                                                           |
| `tests/core`        | `fymo/core`           | HTML rendering, app discovery, config loading, the SSR controller, security defaults                                       |
| `tests/jobs`        | `fymo/jobs`           | task discovery, the job runner, provider registry                                                                          |
| `tests/remote`      | `fymo/remote`         | the `$remote` router, devalue wire format, codegen, opt-in rules                                                           |
| `tests/server`      | `fymo/server`         | dev server, prod serving, the worker sidecar, Dockerfile smoke test                                                        |
| `tests/storage`     | `fymo/storage`        | storage provider registry and the local provider                                                                           |
| `tests/integration` | (none, cross-cutting) | full request/response cycles against `todo_app` and `blog_app`, exercising build, routing, auth, and remote calls together |

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 in `pyproject.toml`:

```toml theme={null}
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
norecursedirs = ["*.egg", ".*", "_darcs", "CVS", "dist", "node_modules", "venv", "{arch}"]
```

To get collected, a file must be named `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 uses `uv`, so you can run the full suite with:

```bash theme={null}
uv run pytest tests/ -v
```

That's the exact command CI runs too, defined in `.github/workflows/ci.yml`.

Narrow things down when you only care about one part of the suite. Run everything in a module:

```bash theme={null}
uv run pytest tests/core/
```

Run a single file:

```bash theme={null}
uv run pytest tests/core/test_html.py
```

Or run a single test:

```bash theme={null}
uv run pytest tests/core/test_html.py::test_minimal_html_structure
```

<Note>
  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.
</Note>

## 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.

```python theme={null}
def test_minimal_html_structure():
    assets = RouteAssets(
        ssr="ssr/todos.mjs",
        client="client/todos.A1B2.js",
        css="client/todos.A1B2.css",
        preload=["client/chunk-datefns.X9Y8.js"],
    )
    html = build_html(
        body="<div class='todo-app'>hi</div>",
        head_extra="",
        props={"todos": []},
        assets=assets,
        title="Todos",
        asset_prefix="/dist",
    )
    assert "<!DOCTYPE html>" in html
    assert '<link rel="stylesheet" href="/dist/client/todos.A1B2.css">' in html
```

The `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:

<CardGroup cols={2}>
  <Card title="example_app / blog_app" icon="copy">
    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`.
  </Card>

  <Card title="node_available" icon="hexagon-check">
    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`.
  </Card>

  <Card title="_fymo_secret_for_tests" icon="key">
    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.
  </Card>

  <Card title="_reset_remote_router_globals" icon="rotate">
    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.
  </Card>
</CardGroup>

<Accordion title="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.
</Accordion>
