FastMCP Tests
Running Tests
Test Organization
Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you’re working onfastmcp_slim/fastmcp/server/auth.py, you’ll find its tests in tests/server/test_auth.py. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they’re split across multiple files.
Test Markers
We use pytest markers to categorize tests that require special resources or take longer to run:subprocess_heavy, exists specifically for Windows CI stability. See Windows CI and Test Parallelism below for when to use it and why it exists.
Windows CI and Test Parallelism
Windows CI ran the unit suite serially for months. #2715 tried enablingpytest-xdist parallelism there in December 2025; #2726 reverted it the next day because “Windows tests continue to fail with intermittent worker crashes.” #4554 re-enabled it after removing most of the subprocess pressure that caused those crashes, taking the Windows unit step from roughly 460s to 175s.
That pressure came from three sources, all addressed by #4554: most HTTP tests moved in-process via asgi_client instead of binding real sockets, stdio lifecycle tests spawn a minimal stdlib responder (tests/client/minimal_stdio_server.py, ~0.03s to start) instead of a subprocess that runs import fastmcp (~0.7s), and roughly 80 real sleep() calls became deterministic waits on the condition each test actually cared about. Fewer, cheaper subprocesses competing under parallel workers left fewer chances for a worker to die.
The subprocess_heavy marker
One class of test still spawns a full Python interpreter that imports FastMCP — checking that a bare install doesn’t need optional dependencies, or that a decorator works from a fresh process. Each spawn pays a full interpreter’s startup and memory footprint, and a 2-core Windows runner already running 2 xdist workers has little headroom left to absorb that. These tests carry @pytest.mark.subprocess_heavy and run in the existing serial client_process CI step instead of alongside the parallel workers — .github/actions/run-pytest/action.yml routes client_process or subprocess_heavy to that step (MAX_PROCS=0) and excludes both markers from the parallel unit step.
If a test runs subprocess.run([sys.executable, "-c", ...]), or otherwise starts a fresh interpreter that imports fastmcp, mark it subprocess_heavy. A subprocess that runs a minimal stdlib script with no FastMCP import doesn’t need the marker — it’s the interpreter startup and import that’s expensive, not the subprocess itself.
This is a mitigation, not a proof
There is no root-cause diagnosis behind this fix, only a plausible one. During validation, one Windows run genuinely crashed a worker ontest_fastmcp_imports_without_legacy_httpx — a fresh-interpreter test — with pytest-xdist reporting worker 'gw1' crashed while running '...' after execnet’s channel saw ConnectionResetError: [WinError 10054]. Nothing in that log says why the worker died: memory exhaustion, handle exhaustion, and some Windows-specific subprocess/execnet interaction are all still consistent with what was observed. Marking the fresh-interpreter tests subprocess_heavy made the crash stop recurring, but “it stopped” is not the same as “we know why.”
Treat the next Windows worker crash as a test of this diagnosis. If it lands on a test that is not a fresh-interpreter spawner, the subprocess_heavy theory was wrong — the real problem is subprocess-under-xdist on Windows more generally, and isolating one marker’s worth of tests was never going to fix that. The fallback is one conditional back in run-pytest/action.yml, restoring the pre-#4554 behavior:
Two traps that aren’t Windows-specific
Two test-authoring bugs surfaced while validating this change. Neither is about Windows or parallelism, but both are worth watching for anywhere a realsleep() gets replaced with a wait:
- Match the wait condition to the assertion. A test waited for “any callback fired,” then asserted that a
completedcallback existed. That races, because an earlierworkingnotification satisfies the wait before thecompletedone arrives. A deterministic wait is only as good as the condition it waits on — wait for the thing you actually assert. - Don’t assert on incidental timing. A crash-recovery test asserted “at least one concurrent request fails” while a subprocess restarts, which quietly depended on the restart being slow. Once restart got faster, recovery could beat every in-flight request and the test started failing because the behavior improved. Assert the invariant instead: no hang, and no result served by the dead process.
Writing Tests
Test Requirements
Following these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.Single Behavior Per Test
Each test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.Self-Contained Setup
Every test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.Clear Intent
Test names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.Using Fixtures
Use fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should not open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.Effective Assertions
Assertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.Inline Snapshots
FastMCP usesinline-snapshot for testing complex data structures. On first run of pytest --inline-snapshot=create with an empty snapshot(), pytest will auto-populate the expected value. To update snapshots after intentional changes, run pytest --inline-snapshot=fix. This is particularly useful for testing JSON schemas and API responses.
In-Memory Testing
FastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves. The in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.Mocking External Dependencies
FastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:Testing Network Transports
In-memory testing covers most unit testing needs, but some behavior only exists over HTTP: middleware, authentication, session management, header handling, and SSE streaming. To test those, serve your server over HTTP withasgi_client.
Testing Over HTTP
asgi_client builds your server’s real Starlette app, starts its lifespan, and hands you a connected Client that talks to it over the full HTTP stack. The one thing it skips is the socket: requests are dispatched straight into the ASGI application on the current event loop, so there is no port to bind, no uvicorn to start, and no connection to negotiate. Everything else — middleware, authentication, session management, SSE framing — runs exactly as it does in production.
transport="sse" to exercise the SSE app instead of streamable HTTP, path= to serve on a custom path, headers= and auth= to configure the client’s requests, and any other keyword argument to configure the Client itself.
Sharing One Server Across Tests
When several tests share a server but each needs its own client, useasgi_server in a fixture. It yields an ASGIServer, whose client() method produces a fresh client — with its own session — on demand.
ping, so a test that is about session behavior pins mode="legacy". Every keyword argument client() doesn’t consume itself is passed straight to Client. See protocol negotiation.
For assertions about raw HTTP — status codes, response headers, metadata endpoints — http_client() returns an httpx.AsyncClient bound to the same app. Because nothing is listening on the network, this is the only way to make raw requests; a plain httpx.AsyncClient() cannot reach the server.
transport() returns a StreamableHttpTransport or SSETransport already wired to the in-process app.
Testing on a Real Port
run_server_async starts a real uvicorn server on a real TCP port as a task in the current process and yields its URL. Reach for it only when the subject of the test is the network itself — real sockets, TLS, or a server that must be reachable by something other than an in-process client.
Subprocess Testing (Special Cases)
For tests that require complete process isolation (like STDIO transport or testing subprocess behavior), userun_server_in_process:
run_server_in_process utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it’s slower and harder to debug than in-process testing. FastMCP uses the client_process marker to isolate these tests in CI.
Documentation Testing
Documentation requires the same validation as code. Thejust docs command launches a local Mintlify server that renders your documentation exactly as users will see it:

