Skip to main content
This guide covers the testing philosophy, test types, and how to run tests for CoW Protocol Services.

Testing Philosophy

Always use cargo nextest run instead of cargo test.The CI uses cargo-nextest which handles global state differently than cargo test. Some tests may pass with cargo test but fail in CI, or vice versa.
The project uses just commands for convenience, which automatically use cargo nextest:
just test-unit      # Unit tests
just test-db        # Database tests  
just test-e2e-local # E2E tests on local node
just test-e2e-forked # E2E tests on forked network
just test-driver    # Driver tests

Test Types

Unit Tests

Fast tests that don’t require external dependencies. They test individual functions and modules in isolation.
just test-unit
What’s tested:
  • Pure functions and business logic
  • Data structure operations
  • Serialization/deserialization
  • Mathematical calculations

Documentation Tests

Tests embedded in documentation comments. These ensure code examples in docs stay up-to-date.
just test-doc
Documentation tests use cargo test (not nextest) as nextest doesn’t support doc tests.

Database Tests

Tests that require PostgreSQL. These test database interactions, migrations, and SQL queries.

Prerequisites

1

Start PostgreSQL

docker compose up -d
This starts PostgreSQL on port 5432 and runs migrations automatically.
2

Verify database is running

docker compose ps

Running Database Tests

just test-db
Why --test-threads 1? Database tests modify shared state (database tables). Running them in parallel could cause race conditions and test failures.Why --run-ignored ignored-only? Database tests are marked with #[ignore] because they require external PostgreSQL. This flag runs only ignored tests.

End-to-End (E2E) Tests

E2E tests validate complete workflows by deploying contracts, running services, and executing real transactions. All E2E tests are located in crates/e2e/tests/.

E2E Local Node Tests

Tests that run against a clean local Ethereum node (no forking).
just test-e2e-local
Or with cargo:
cargo nextest run -p e2e local_node \
  --test-threads 1 --failure-output final --run-ignored ignored-only
What’s tested:
  • Order placement and settlement
  • Solver competition
  • Protocol interactions
  • Contract deployments

E2E Forked Network Tests

Tests that fork real networks (Mainnet or Gnosis Chain) to test against production state.

Prerequisites

1

Install Foundry (anvil)

curl -L https://foundry.paradigm.xyz | bash
foundryup
2

Set RPC URLs

You need RPC endpoints for forking:
.env
export FORK_URL_MAINNET="https://mainnet.infura.io/v3/YOUR_KEY"
export FORK_URL_GNOSIS="https://rpc.gnosischain.com"
RPC Providers:

Running Forked Tests

just test-e2e-forked
What’s tested:
  • Integration with real tokens (USDC, DAI, etc.)
  • Real liquidity sources (Uniswap, Balancer)
  • Actual contract addresses
  • Production-like scenarios
The ANVIL_COMMAND environment variable controls the anvil binary path. It defaults to "anvil" which must be in your PATH.

Custom E2E Test Filters

Run specific E2E tests by name or pattern:
just test-e2e "local_node::test_name"
just test-e2e "forked_node" "--nocapture"

Driver Tests

Specialized tests for the driver component that require extra stack space.
just test-driver
Driver tests need RUST_MIN_STACK=3145728 (3MB) because they perform complex simulations that can overflow the default stack.

Flaky Test Workflow

Sometimes tests are flaky and fail intermittently in CI but pass locally. To test your fix:
1

Identify the flaky test

Note the test name from the failing CI run.
2

Run the GitHub Action

Go to: Actions → run-flaky-test in the GitHub repository.This workflow runs the test multiple times to verify stability.
3

Verify the fix

If the test passes consistently in CI, your fix is likely good!
The workflow is defined in .github/workflows/pull-request.yaml.

Test Organization

Tests are organized throughout the codebase:
crates/
├── e2e/              # End-to-end tests
│   ├── tests/        # E2E test files
│   │   ├── forked_node/
│   │   └── local_node/
│   └── src/          # E2E test helpers
├── orderbook/
│   ├── src/          # Source with inline unit tests
│   └── tests/        # Integration tests
├── autopilot/
│   ├── src/          # Source with inline unit tests
│   └── tests/        # Integration tests
└── testlib/          # Shared test utilities

Test Helpers

testlib crate: Provides shared utilities for writing tests
  • Mock HTTP servers
  • Test fixtures
  • Common assertions
  • Helper functions
crates/e2e/src: E2E-specific helpers
  • Node setup
  • Contract deployment
  • Service initialization

Running All Tests (Like CI)

To run the complete test suite locally:
# 1. Documentation tests
just test-doc

# 2. Unit tests
just test-unit

# 3. Database tests (requires postgres)
docker compose up -d
just test-db

# 4. E2E local tests
just test-e2e-local

# 5. E2E forked tests (requires FORK_URL_* env vars)
export FORK_URL_MAINNET="..."
export FORK_URL_GNOSIS="..."
just test-e2e-forked

# 6. Driver tests
just test-driver
Running all tests takes 30-60 minutes. Most development only requires unit tests and relevant integration tests.

Writing Tests

Unit Test Example

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_order_validation() {
        let order = Order::default();
        assert!(order.validate().is_ok());
    }

    #[tokio::test]
    async fn test_async_function() {
        let result = fetch_data().await;
        assert!(result.is_ok());
    }
}

Database Test Example

#[tokio::test]
#[ignore] // Requires PostgreSQL
async fn test_insert_order() {
    let db = testlib::pg_pool().await;
    let order = Order::default();
    
    let result = db.insert_order(&order).await;
    assert!(result.is_ok());
}

E2E Test Example

#[tokio::test]
#[ignore] // E2E test
async fn test_order_settlement() {
    let services = testlib::start_services().await;
    
    // Place order
    let order_uid = services.create_order(/* ... */).await;
    
    // Wait for settlement
    services.wait_for_settlement(order_uid).await;
    
    // Verify results
    let order = services.get_order(order_uid).await;
    assert_eq!(order.status, OrderStatus::Fulfilled);
}

Test Coverage

While the project doesn’t enforce coverage metrics, aim for:
  • High coverage of business logic
  • Critical paths well-tested
  • Edge cases covered
  • Error conditions tested

Best Practices

Keep tests fast

Unit tests should run in milliseconds. Use mocks for external dependencies.

Test one thing

Each test should verify a single behavior or scenario.

Use descriptive names

Test names should clearly describe what they test: test_order_fails_when_expired

Avoid flaky tests

Don’t rely on timing, random data, or external state that can change.

Continuous Integration

The CI runs on every PR and push to main. See .github/workflows/pull-request.yaml for the complete pipeline:
  1. Lint: Formatting and clippy checks
  2. Doc tests: Documentation examples
  3. Unit tests: Fast unit tests
  4. Database tests: PostgreSQL integration
  5. E2E tests: Both local and forked
  6. Driver tests: Driver-specific tests
  7. Security: Trivy and cargo-audit scans

Troubleshooting

Likely causes:
  • Using cargo test instead of cargo nextest run
  • Test depends on local state or timing
  • Test needs --test-threads 1 but runs in parallel
Solution:
cargo nextest run --test-threads 1
Check PostgreSQL is running:
docker compose ps
docker compose logs db
Reset database:
docker compose down --volumes
docker compose up -d
Check RPC URLs are set:
echo $FORK_URL_MAINNET
echo $FORK_URL_GNOSIS
Use a reliable RPC:
  • Free public RPCs can be slow or rate-limited
  • Consider using Infura, Alchemy, or running a local node
Ensure RUST_MIN_STACK is set:
RUST_MIN_STACK=3145728 cargo nextest run -p driver

Next Steps

Setup Guide

Set up your development environment

Playground

Test changes in a full local environment

Contributing

Submit your changes

E2E Tests

Detailed E2E test reference

Build docs developers (and LLMs) love