This document describes how to run and write backend tests for actix-api.
All tests live inside the source files as #[cfg(test)] modules — there is no
separate tests/ directory. Tests run against real PostgreSQL and NATS
instances provided by Docker Compose.
- Docker and Docker Compose (v2)
That's it. The test runner, database, and message broker all run in containers.
# Build + run all backend tests (spins up PostgreSQL + NATS in Docker)
make tests_run
# Tear down test containers and volumes
make tests_down
# Rebuild the test Docker image (after Dockerfile changes)
make tests_buildmake tests_run does two things in sequence: first it brings up the Docker
Compose stack (postgres, nats, rust-tests), then it runs the rust-tests
service which applies database migrations and executes cargo test.
To run a single test you can override the command:
docker compose -f docker/docker-compose.integration.yaml run --rm rust-tests \
bash -c "cd /app/dbmate && dbmate wait && dbmate up && \
cd /app/actix-api && cargo test test_meeting_creation -- --nocapture --test-threads=1"Tests are orchestrated by docker/docker-compose.integration.yaml, which
provides three services:
| Service | Image | Purpose |
|---|---|---|
postgres |
postgres:12 |
Database for meetings, sessions, and participants |
nats |
nats:2.10-alpine |
Message broker with JetStream enabled |
rust-tests |
Built from docker/Dockerfile.actix |
Test runner container |
The rust-tests container mounts the repo at /app and runs:
dbmate wait && dbmate up— waits for PostgreSQL, applies migrationscargo test -p videocall-api -- --nocapture --test-threads=1
Note: make tests_run (from the Makefile) additionally runs cargo clippy,
cargo fmt --check, and cargo machete before tests. The docker-compose
command in the yaml file runs only the test step.
Tests execute single-threaded (--test-threads=1) because they share a
database and use #[serial_test::serial] to prevent race conditions.
Tests that touch the database clean up at the start and end using a helper like
this (from src/webtransport/mod.rs):
async fn cleanup_room(pool: &sqlx::PgPool, room_id: &str) {
let _ = sqlx::query("DELETE FROM session_participants WHERE room_id = $1")
.bind(room_id)
.execute(pool)
.await;
let _ = sqlx::query("DELETE FROM meetings WHERE room_id = $1")
.bind(room_id)
.execute(pool)
.await;
}Tests toggle feature flags explicitly and clean up after themselves:
FeatureFlags::set_meeting_management_override(true);
// ... test logic ...
FeatureFlags::clear_meeting_management_override();Each test uses a unique room ID (e.g. "test-room-create-1",
"test-room-join-2") to avoid conflicts when tests run sequentially.
Several helpers simplify integration testing:
get_test_pool()— creates a database connection pool fromDATABASE_URLwait_for_participant_count(pool, room, expected, timeout)— polls until the expected number of participants is reachedstart_webtransport_server()/start_websocket_server()— starts test servers on ephemeral ports
Tests run automatically via .github/workflows/cargo-test.yaml, triggered on
PRs that touch actix-api/, videocall-types/, or protobuf/. The workflow
calls make tests_run and always tears down with make tests_down.
- Add a
#[cfg(test)]module at the bottom of the source file being tested. - Use
#[serial_test::serial]if the test touches the database or shared state. - Create unique room/user IDs to avoid collisions with other tests.
- Clean up database rows at the start and end of the test.
- Run
make tests_runto verify.