Skip to main content
You're viewing v3 documentation

This is the v3 HyperIndex documentation. Still on an older version? Open the v2 documentation and consider migrating to v3.

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:

  1. Generate greeter template in TypeScript using Envio CLI
pnpx envio@3.0.0-rc.0 init template -l typescript -d greeter -t greeter -n greeter
  1. Run tests
pnpm test
  1. See the src/indexer.test.ts file 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

  1. Install Vitest (recommended):
pnpm add -D vitest
  1. Add a test file (e.g., src/indexer.test.ts)

  2. Add a test command to your package.json

"test": "vitest run",
  1. 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:

  1. Create a test indexer with createTestIndexer()
  2. Call indexer.process({ chains: { id: { simulate: [...] } } }) with one or more events
  3. Read entities back via await indexer.Entity.getOrThrow(id) (or .get(id))
  4. 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 match
  • expect(actualEntity.property).toBe(expectedValue) — Verify specific property values
  • expect(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);
});