Best Practices for Testing Microservices with Local Databases in Rust

Use Docker Compose to spin up PostgreSQL containers locally, configure services via environment variables, and run tests with just test for fast, realistic integration testing of the Macro microservices platform.

The Macro repository is a Rust-based microservices platform that relies on multiple PostgreSQL databases—MacroDB, CommsDB, EmailDB, and ContactsDB—for persistence. Testing these services against real database behavior rather than mocks is essential for catching serialization errors, transaction bugs, and schema mismatches before deployment. This guide covers the workflow used in the macro-inc/macro codebase to achieve reliable, fast local testing.

Start the Local Database Stack with Docker Compose

The foundation of local testing is the docker/docker-compose.yml file at the repository root. This file defines PostgreSQL containers, Redis, networking, ports, and persistence volumes that mirror production conditions.

docker compose -f docker/docker-compose.yml up -d postgres redis

Bringing up containers takes seconds and provides the same driver (sqlx) and connection handling code used in production. This eliminates the "works on my machine" gap between development and deployment environments.

Configure Environment Variables for Database Connections

Services in Macro load database connection strings through the macro_env_var crate. The .env.example file in the repository root shows the required variables:

  • POSTGRES_URL
  • REDIS_URL

For local testing, export these variables pointing to your Docker Compose containers:

export POSTGRES_URL=postgres://macro:macro@localhost:5432/macro
export REDIS_URL=redis://localhost:6379

The just setup_test_envs helper can automate this population, loading values appropriate for the local Docker stack.

Run Tests with the Repository-Wide just test Command

The justfile at the repository root provides a unified interface for testing. The test recipe executes cargo test -p {service} for each microservice, running integration tests that communicate with live PostgreSQL containers.

Run tests for a specific service:

just test -p document_storage_service

Run the entire workspace:

just test

Under the hood, the test harness also invokes just prepare_db to refresh the SQLx query cache after migrations. This ensures compile-time SQL validation stays synchronized with the actual database schema.

Keep Database Schema in Sync with Migrations

Whenever you add or modify a migration, you must update the schema and SQLx cache before testing. The migration files live in crates/macro_db_client/migrations/.

Run these commands after any schema change:

just migrate_db    # Apply pending migrations to the local database

just prepare_db    # Update the SQLx offline cache with new query metadata

just test -p {service}

The prepare_db step populates the .sqlx directory, enabling compile-time query validation without a live database during cargo check. During actual test execution, the cache is refreshed so live queries match the current schema.

Isolate Tests by Service with Async Test Modules

Each microservice in Macro resides in its own crate with a dedicated test module. Tests use #[tokio::test] attributes to spin up an async runtime and interact with the database concurrently.

Example structure from the codebase:

This isolation prevents cross-service pollution while still allowing realistic integration testing against the shared local database instances.

Validate in CI with Identical Infrastructure

The GitHub Actions workflow at .github/workflows/ci.yml runs the same Docker Compose stack used locally. This guarantees that every pull request is validated against identical database environments, eliminating environment-specific failures.

The CI pipeline executes:

  1. docker compose -f docker/docker-compose.yml up -d for services
  2. just migrate_db and just prepare_db for schema synchronization
  3. just test for the full test suite

Complete Local Testing Workflow

Here is the minimal command sequence for testing any Macro microservice:


# 1. Start required containers

docker compose -f docker/docker-compose.yml up -d postgres redis

# 2. Export test environment variables

export POSTGRES_URL=postgres://macro:macro@localhost:5432/macro
export REDIS_URL=redis://localhost:6379

# 3. Synchronize schema and cache

just migrate_db
just prepare_db

# 4. Run service-specific tests

just test -p document_storage_service

After adding a new migration, repeat steps 3 and 4 before committing:

just migrate_db && just prepare_db && just test -p {service}

Summary

  • Use docker/docker-compose.yml to provision realistic PostgreSQL and Redis containers locally
  • Load connection strings via the macro_env_var crate with variables shown in .env.example
  • Execute tests through just test for consistent, cached, service-isolated runs
  • Synchronize schema with just migrate_db and just prepare_db after any migration changes
  • Mirror CI locally—the same commands in .github/workflows/ci.yml run on every pull request

Frequently Asked Questions

Why use real PostgreSQL containers instead of mocks or in-memory databases?

Real containers exercise the same sqlx driver, connection pooling, and transaction behavior used in production. This catches serialization failures, deadlock scenarios, and schema mismatches that in-memory SQLite or mocks cannot replicate, as implemented in macro-inc/macro.

How does SQLx offline mode work with local testing?

The just prepare_db command populates the .sqlx cache with query metadata from your live database. This enables cargo check and builds without database connectivity. During just test, the cache is refreshed against the live containers so runtime queries match the current schema, stored in crates/macro_db_client/migrations/.

Can I run tests for only one service without starting all containers?

No. Even single-service tests require the PostgreSQL container for that service's database. However, just test -p {service} limits test execution to one crate, keeping feedback fast. The Docker Compose startup completes in seconds, minimizing overhead for targeted testing.

What happens if I forget to run prepare_db after a migration?

Tests may fail at compile time with SQLx query validation errors, or at runtime with schema mismatches. The just test recipe automatically invokes prepare_db, but manual runs of cargo test without the justfile require explicit cache updates via just prepare_db to stay synchronized.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →