Testing the Server
Writing pytest tests for routes and database operations
Concepts
- A test calls code with specific inputs and asserts
the output matches what is expected
- if the assertion fails, the test fails and you know something broke
create_test_clientcreates an in-process HTTP client connected to your Litestar application- make requests and get responses without starting a real server or opening a network socket
- A pytest fixture is a function decorated with
@pytest.fixturethat creates and optionally tears down a resource (like a database connection) before and after each test- prevents setup code from being repeated and ensures tests are isolated
- An in-memory SQLite database (opened with
":memory:") is created fresh for each test and destroyed when the connection closes- tests never interfere with each other or with the production database
- Test what matters: application logic (query results, validation rules,
route behavior)
- do not test Litestar's routing, SQLite's SQL parsing, or one-line functions that are obviously correct
Why test?
- Reproducibility is a research value
- A test that catches a regression is worth more than a comment that promises correctness
Testing a route with Litestar's test client
create_test_client, making GET and POST requests- Asserting on status codes and response bodies
A test database fixture
- A pytest fixture that creates a fresh in-memory SQLite database
- Populates it with a few rows and tears it down after each test
Testing queries
- Calling query functions directly, not through HTTP
- Asserting on the rows returned by a
SELECTand the side effects of anINSERT - What not to test: the framework, third-party libraries, and code that is trivially correct
Check for Understanding
What does create_test_client do, and why don't you need to start a real server to use it?
create_test_client wraps your Litestar application in an in-process HTTP client. When
you call client.get("/snails"), it routes the request through Litestar's routing layer
and calls your handler directly, without opening a network socket or starting a server
process. The response object behaves like a real HTTP response (status code, headers,
body), but the entire round-trip happens in memory. This makes tests fast and removes
the need for port management or server startup.
What is a pytest fixture, and why is it better to use one for database setup rather than repeating the setup code in every test?
A fixture is a function that creates a resource and (optionally) cleans it up after the test. Any test that lists the fixture's name as a parameter automatically receives the created resource. This avoids copy-pasting setup code into every test function, ensures setup and teardown are always paired, and makes it easy to change the setup in one place if the database schema changes.
What is an in-memory SQLite database, and what happens to its contents when the connection closes?
An in-memory database is created by passing ":memory:" as the database path to
sqlite3.connect(). It exists only in RAM; nothing is written to disk. When the
connection is closed (at the end of the test), the database and all its contents
are permanently gone. This makes it ideal for tests: each test gets a clean slate
with no left-over data from previous tests.
Give an example of something you should not test and explain why testing it adds no value.
You should not test that Litestar returns a 200 status code for a valid route—that
is testing the framework, not your code. You should not test that sqlite3.connect()
opens a connection—that tests the standard library. You should not test a function
that contains only return a + b—the code is so simple that a test adds no information.
Test your logic: does the query return the right rows when filtered? Does the validator
reject an out-of-range diameter?
Exercises
Test pagination
Write tests that verify: (1) the first page returns the first page_size rows, (2)
the second page returns the next page_size rows, and (3) requesting a page beyond
the last one returns an empty result or a 404. Use the test database fixture to
control exactly how many rows exist.
Test filtering
Write tests for the search route that verify: (1) filtering by site name returns only rows matching that site, (2) filtering by mutation status returns only matching rows, (3) applying both filters at once returns only rows that match both. The tests must assert on the content of the response, not just the status code.
Test error responses
Write tests that send a POST request to the add-record route with: (1) a missing required field, (2) a diameter value below the minimum, (3) a site name not in the database. Assert that each returns a 400 status code and that the response body contains the name of the field that failed validation.
Measure coverage
Run pytest --cov=. --cov-report=term-missing and examine the output. Find the
three largest blocks of untested code. For each one, decide: is it worth writing a
test for this? If yes, write it. If no, explain why the code is low risk or is
covered by other means.