For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content

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.

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

  1. Write a failing test with expected entity output
  2. Implement the handler until the test passes
  3. Capture the snapshot — run pnpm test to fill toMatchInlineSnapshot
  4. Review and commit the snapshot for regression testing
warning

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