Testing
Introduction
Envio ships with a built-in testing library that doubles as a development loop. createTestIndexer() runs your real handlers in-process, so you can iterate on logic and validate behavior without deploying anywhere. It's designed for:
- TDD: Write a failing test, implement the handler, capture the snapshot, commit
- Unit tests: Feed synthetic events directly into handlers to exercise edge cases in isolation
- E2E tests against real blockchain data: Pin a block range or let the indexer auto-detect the first block with events, and run your full handler pipeline end-to-end
- Regression-proof assertions: Inspect entities and per-block change sets, then lock in expected output with
toMatchInlineSnapshot
The library integrates well with Vitest (recommended) and any other JavaScript-based testing framework.
Getting Started
The simplest way to start is auto-exit mode — no block ranges, no mock events. The indexer automatically finds the first block with events and processes it.
import { describe, it } from "vitest";
import { createTestIndexer } from "envio";
describe("Indexer Testing", () => {
it("Should process first two blocks with events", async (t) => {
const indexer = createTestIndexer();
t.expect(
await indexer.process({ chains: { 1: {} } }),
"Should find the first block with an event on chain 1 and process it."
).toMatchInlineSnapshot(``);
t.expect(
await indexer.process({ chains: { 1: {} } }),
"Should find the second block with an event on chain 1 and process it."
).toMatchInlineSnapshot(``);
});
});
Run pnpm test — Vitest auto-fills the snapshots on first run. Review and commit them.
Process API
indexer.process({ chains }) is the single entry point for driving the indexer. The shape of each chain entry determines the mode.
Auto-exit (recommended for getting started)
Processes the first block with matching events for each chain, then exits. Each subsequent call continues from where the previous one stopped.
const result = await indexer.process({
chains: {
1: {}, // auto-detect first block with events on chain 1
8453: {}, // same for chain 8453
},
});
Explicit block range
Process a specific block range. Use when you need deterministic, pinned snapshots.
const result = await indexer.process({
chains: {
1: { startBlock: 10_000_000, endBlock: 10_000_100 },
},
});
Simulate (mock events)
Feed synthetic events without hitting the network. Best for unit-testing handler logic.
await indexer.process({
chains: {
1: {
simulate: [
{
contract: "ERC20",
event: "Transfer",
params: { from: addr1, to: addr2, value: 100n },
},
],
},
},
});
You can pass multiple events in a single simulate array — they will be processed in order, just like in production.
You can optionally specify detailed event metadata per simulated event using the same block / transaction / srcAddress / logIndex shape that real events expose. See field_selection for the full list of overridable fields.
result.changes
result.changes is an array of per-block change objects. Each entry has block, chainId, eventsProcessed, plus entity names as keys with sets arrays of created/updated entities. Dynamic contract registrations appear under addresses.sets.
Entity State API
Preset state before processing and read entities after.
// Preset state before processing
indexer.EntityName.set({ id: "...", field: value });
// Read state after processing
await indexer.EntityName.get("id"); // returns entity | undefined
await indexer.EntityName.getOrThrow("id"); // throws if not found
await indexer.EntityName.getAll(); // returns all entities of this type
Assertions
The testing library works with any JavaScript assertion library. The examples below use Vitest's built-in expect.
// Snapshot (recommended — captures full output, auto-filled on first run)
t.expect(result.changes).toMatchInlineSnapshot(`...`);
// Entity assertions
const pool = await indexer.Pool.getOrThrow(poolId);
t.expect(pool).toEqual({ id: poolId, token0_id: "0xabc..." });
// Count
t.expect(result.changes[0]?.Pair?.sets).toHaveLength(1);
// Contract addresses (after dynamic registration)
t.expect(indexer.chains[1].MyContract.addresses).toContain("0x1234...");
TDD Workflow
- Write a failing test with expected entity output
- Implement the handler until the test passes
- Capture the snapshot — run
pnpm testto filltoMatchInlineSnapshot - Review and commit the snapshot for regression testing
Do not add tests which simply restate the implementation. These provide zero confidence.
Running Tests
pnpm test # Run all tests
pnpm test -- -u # Update snapshots