Testing
Tests in base/base are organized into four tiers. Each step up the ladder puts more of the production system under test and costs proportionally more time to run. Knowing which tier a change belongs in is most of the work: reach for the fastest one that actually covers the behavior you changed.
For the day-to-day just recipes themselves, see Development Workflow.
Before Opening a Pull Request
Section titled “Before Opening a Pull Request”One command covers the full local gate:
just ciIt applies the auto-fixers first, then runs every static check and the complete unit test suite:
fix → check::all (format, clippy, udeps, deny) → test → lychee → zepter → check::no-std → check::no-std-proofWhen you have only touched a handful of crates, there is a narrower variant that scopes clippy and the test run to the crates your branch actually affects, relative to main:
just prBoth need Docker, just, and Foundry (forge) available on your machine — see Prerequisites.
The Four Tiers
Section titled “The Four Tiers”| Tier | Speed | Scope | Location |
|---|---|---|---|
| Unit | milliseconds | One function or type on its own | Colocated #[cfg(test)] mod tests blocks |
| Action | milliseconds | Real protocol logic — batching, derivation — against in-memory actors | actions/harness (base-action-harness) |
| System | minutes | The whole L1 + L2 stack under Docker/testcontainers | etc/systems (base-system-tests) |
| Fuzz | hours (nightly) | Randomized transaction streams hunting sync-parity regressions | base-system-tests, nightly only |
Unit Tests
Section titled “Unit Tests”Unit tests sit in the same file as the code under test, in a #[cfg(test)] mod tests { ... } block placed at the end. Run the whole workspace suite with:
just testBehind that recipe is cargo nextest run --workspace --all-features --exclude base-system-tests --no-fail-fast, preceded by a build of the test contracts and SP1 ELFs. The system-test crate is excluded here because it is covered by its own tier below.
To limit the run to crates your branch touches:
just test-affectedAction Tests
Section titled “Action Tests”Action tests are the middle tier: an integration framework that models the L1 block producer, batcher, sequencer, and verifier as lightweight in-memory actors, drives them through a scripted list of actions, and asserts on the chain state that results. Nothing real is spun up — no nodes, no sockets, no containers — but batch encoding, channel compression, and derivation all go through the same production types they would in a live network. That combination is the point: protocol-boundary bugs that slip past unit tests get caught in milliseconds instead of minutes.
just actions testOr invoke the harness crate directly:
cargo nextest run -p base-action-harnessThe actions/README.md in the repository documents the actor architecture and how to add a scenario.
System Tests
Section titled “System Tests”System tests bring up an isolated L1 + L2 stack with testcontainers and drive the node end to end against real components — Reth for L1, Lighthouse, and real Base sequencer and validator processes. It is the slowest and most complete tier:
just devnet testsThat recipe builds the test contracts, then runs cargo nextest run -p base-system-tests. Docker is required.
Because they are expensive, system tests are skipped on pull requests and run on the merge queue instead. Every commit passes through the queue before it lands, so main still never accrues an untested integration path — the cost just moves off the PR feedback loop.
Fuzz Tests
Section titled “Fuzz Tests”A scheduled nightly job throws randomized transaction streams at the node to shake out sync-parity regressions, spreading the work over 4 parallel shards. Every shard draws its own random seed and logs it, so any failure can be replayed deterministically:
cargo nextest run -P ci -p base-system-tests --cargo-profile ci --no-capture -E 'test(fuzz_sync_parity)'Neither pull requests nor the merge queue trigger it. It fires on the daily schedule at 07:00 UTC, or through a manual workflow_dispatch when you want to replay a particular seed.
Static Checks
Section titled “Static Checks”Alongside the tests, just ci and just pr run a set of static checks:
| Check | Command | Purpose |
|---|---|---|
| Format | cargo +nightly fmt --all -- --check | Enforces rustfmt.toml (2024 edition style) |
| Clippy | cargo clippy --workspace --all-features --all-targets -- -D warnings | Lints, warnings denied |
| Unused deps | cargo +nightly udeps --locked --workspace --all-features --all-targets | Flags unused Cargo.toml dependencies |
| Dependency bans/licenses | cargo deny check bans --hide-inclusion-graph | Enforces deny.toml — allowed licenses, banned crates, source restrictions |
no_std | etc/scripts/ci/check-no-std.sh | Confirms no_std crates still build without std |
no_std (proof) | etc/scripts/ci/check-no-std-proof.sh | The same, for the bare-metal FPVM proof crates |
| Feature flags | zepter format features && zepter | Validates Cargo feature propagation across the workspace |
| Links | lychee --config ./lychee.toml . | Looks for dead links repository-wide |
Every one of these has its own just check::<name> recipe — just check::clippy, just check::udeps, and so on. Run just check for the full list, and just fix to auto-repair formatting, clippy, and zepter issues where that is possible.
CI Stages
Section titled “CI Stages”Which checks run depends on where a change sits in its lifecycle:
| Stage | Trigger | Workflow | Scope |
|---|---|---|---|
| Pull request | pull_request | ci-pr.yml → ci-core.yml | Build/clippy/test, affected crates only vs. the base branch |
| Pull request | pull_request | no-std.yml, zepter.yml, lychee.yml, action-tests.yml, base-std-fork-tests.yml | Full workspace — these are already fast |
| Merge queue | merge_group | ci-merge-queue.yml → ci-core.yml | Full workspace build/clippy/test, plus system tests |
Push to main | push | ci-main-cache.yml | Warms the shared Rust build cache |
| Nightly | schedule (07:00 UTC) | fuzz-nightly.yml | Sharded sync-parity fuzzing |
| Nightly | schedule (13:00 UTC) | udeps-report.yml | Unused-dependency report; opens a GitHub issue on findings |
| Release | manual / push to releases/v* | See Release Process | Release builds, RC tags, Docker images |
Pull requests scope to affected crates through etc/scripts/local/affected-crates.py, which is exactly why the two local recipes differ: just pr mirrors what PR CI will do, while just ci mirrors the broader bar the merge queue eventually enforces.
One naming trap worth knowing: action-tests.yml is not a workflow that tests GitHub Actions, despite how it reads. It drives the action-test tier covered earlier on this page, via just actions::lint-ci and just actions::test-ci, and does so for both pull requests and merge-queue runs.
Conventions
Section titled “Conventions”- Any behavioral change should come with tests that cover it.
- Pick the cheapest tier that genuinely exercises what you changed. Logic that stands on its own belongs in a unit test; anything crossing a protocol boundary — batching, derivation, channel encoding — belongs in an action test; reserve system tests for the cases that truly need a live L1/L2 stack.
- Keep unit tests next to the implementation, in a trailing
#[cfg(test)] mod tests { ... }block. - Run
just prwhile iterating for quick feedback, andjust cibefore you ask for review, so nothing surprises you at the merge queue.
Command Reference
Section titled “Command Reference”| Command | Runs |
|---|---|
just ci | The full local gate — fix, all checks, full test suite |
just pr | Affected-crates-only variant of just ci |
just fix | Auto-fixes formatting, clippy, and zepter issues |
just test | Unit tests across the whole workspace |
just test-affected | Unit tests for affected crates only |
just actions test | Action tests (base-action-harness) |
just devnet tests | System tests (base-system-tests, needs Docker) |
just check | Lists every check::* static-check recipe |
just lychee | Link check |
just zepter | Feature-flag validation |
Next Steps
Section titled “Next Steps”- Development Workflow — the everyday check, test, and fix recipes.
- Local Devnet — bring up a full local network for integration work.