Testing
Introduction
Envio comes with a built-in testing library that enables developers to thoroughly validate their indexer behavior without requiring deployment or interaction with actual blockchains. This library is specifically crafted to:
- Spin up an in-process indexer: Use
createTestIndexer()to run your real handlers against an in-memory database - Simulate blockchain events: Pass event params directly to
indexer.process({ chains: { id: { simulate: [...] } } }) - Assert event handler logic: Verify that your handlers correctly process events and update entities
- Test complete workflows: Validate the entire process from event creation to database updates
The testing library integrates well with Vitest (recommended) and any other JavaScript-based testing framework.
Learn by doing
If you prefer to explore by example, the Greeter template includes complete tests that demonstrate best practices:
- Generate
greetertemplate in TypeScript using Envio CLI
pnpx envio@3.0.0-rc.0 init template -l typescript -d greeter -t greeter -n greeter
- Run tests
pnpm test
- See the
src/indexer.test.tsfile to understand how the tests are written.
Getting Started
This section covers how to set up testing for your existing HyperIndex indexer.
Prerequisites
- A functioning indexer setup with schema and event handlers
- Envio CLI v3 (verify with
envio -V)
Setup Steps
- Install Vitest (recommended):
pnpm add -D vitest
-
Add a test file (e.g.,
src/indexer.test.ts) -
Add a test command to your
package.json
"test": "vitest run",
- Generate the testing types by running:
pnpm codegen
This regenerates .envio/types.d.ts based on your schema and configuration. Always run this command when you make changes to your schema or configuration files.
Writing tests
Test Library Design
The testing library follows key design principles that make it effective for testing HyperIndex indexers:
- Real handlers, in-memory storage:
createTestIndexer()runs your actual registered handlers against an in-memory store, so tests exercise the same code paths as production. - Batched simulation: Pass any number of events to
indexer.process({ chains: { id: { simulate: [...] } } })and they will run through the handler pipeline in order. - Realistic simulations: Simulated events closely mirror real blockchain events.
Typical Test Flow
Most tests will follow this general pattern:
- Create a test indexer with
createTestIndexer() - Call
indexer.process({ chains: { id: { simulate: [...] } } })with one or more events - Read entities back via
await indexer.Entity.getOrThrow(id)(or.get(id)) - Assert that the resulting state matches your expectations
This flow allows you to verify that your event handlers correctly create, update, or modify entities in response to blockchain events.
API
The envio package exposes the test entry points directly:
createTestIndexer
Creates a fresh in-process indexer wired up to your registered handlers:
import { createTestIndexer } from "envio";
const indexer = createTestIndexer();
indexer.process({ chains })
Runs one or more simulated events through your handlers. Provide a chains map keyed by chain id, with a simulate array of { contract, event, params } objects:
await indexer.process({
chains: {
137: {
simulate: [
{
contract: "Greeter",
event: "NewGreeting",
params: { greeting: "Hi", user: userAddress },
},
],
},
},
});
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.
Reading entities
Entity helpers live directly on the indexer:
// Throw if missing
const user = await indexer.User.getOrThrow(userAddress);
// Or fall back to undefined
const maybeUser = await indexer.User.get(userAddress);
You can also pre-seed entities before processing:
indexer.User.set({
id: userAddress,
latestGreeting: "preexisting",
numberOfGreetings: 3,
greetings: ["preexisting"],
});
Assertions
The testing library works with any JavaScript assertion library. The examples below use Vitest's built-in expect, but you can also use Node.js's assert, chai, or any other library.
Common assertion patterns include:
expect(actualEntity).toEqual(expectedEntity)— Check that entire entities matchexpect(actualEntity.property).toBe(expectedValue)— Verify specific property valuesexpect(await indexer.Entity.get(id)).toBeDefined()— Ensure an entity exists
Examples
A NewGreeting Event Creates a User Entity
This example tests that when a NewGreeting event is processed, it correctly creates a new User entity:
import { describe, it, expect } from "vitest";
import { createTestIndexer, type User, TestHelpers } from "envio";
const { Addresses } = TestHelpers;
it("A NewGreeting event creates a User entity", async () => {
// Step 1: Create an in-process indexer
const indexer = createTestIndexer();
// Step 2: Define test data
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";
// Step 3: Simulate the event through your handler
await indexer.process({
chains: {
137: {
simulate: [
{
contract: "Greeter",
event: "NewGreeting",
params: { greeting, user: userAddress },
},
],
},
},
});
// Step 4: Define what we expect to see in storage
const expectedUserEntity: User = {
id: userAddress,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
};
// Step 5: Verify the indexer state matches what we expect
const actualUserEntity = await indexer.User.getOrThrow(userAddress);
expect(actualUserEntity).toEqual(expectedUserEntity);
});
Testing Entity Updates: 2 Greetings from the Same User
This example tests that when the same user sends multiple greetings, the counter increments correctly:
import { describe, it, expect } from "vitest";
import { createTestIndexer, TestHelpers } from "envio";
const { Addresses } = TestHelpers;
it("2 Greetings from the same users results in that user having a greeter count of 2", async () => {
// Step 1: Create an in-process indexer
const indexer = createTestIndexer();
// Step 2: Define test data for two events
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";
const greetingAgain = "Oh hello again";
// Step 3: Process both events through the handlers
await indexer.process({
chains: {
137: {
simulate: [
{
contract: "Greeter",
event: "NewGreeting",
params: { greeting, user: userAddress },
},
{
contract: "Greeter",
event: "NewGreeting",
params: { greeting: greetingAgain, user: userAddress },
},
],
},
},
});
// Step 4: Get the entity and verify the greeting count is 2
const actualUserEntity = await indexer.User.getOrThrow(userAddress);
expect(actualUserEntity.numberOfGreetings).toBe(2);
});