Skip to main content

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:

  • Mock database states: Create and manipulate in-memory representations of your database
  • Simulate blockchain events: Generate test events that mimic real blockchain activity
  • 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 provides helper functions that integrate with any JavaScript-based testing framework (like Mocha, Jest, or others), giving you flexibility in how you structure and run your tests.

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 init template -l typescript -d greeter -t greeter -n greeter
  1. Run tests
pnpm test
  1. See the test/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 version 0.0.26 or above (verify with envio -V)

Setup Steps

  1. Install your preferred testing framework, for example Mocha
pnpm i mocha
  1. Create a test folder and a test file (e.g., test.js) inside it

  2. Add a test command to your package.json

"test": "mocha",
  1. Generate the testing library by running:
pnpm codegen

This command will generate the TestHelpers file that contains the testing API 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:

  • Immutable database: The mock database is immutable, with each operation returning a new instance. This makes it robust and easy to test against previous states.
  • Chainable operations: Operations can be chained together to build complex test scenarios.
  • Realistic simulations: Mock events closely mirror real blockchain events, allowing you to test your handlers in conditions similar to production.

Typical Test Flow

Most tests will follow this general pattern:

  1. Initialize the mock database (empty or with predefined entities)
  2. Create a mock event with test parameters
  3. Process the mock event through your handler(s)
  4. Assert that the resulting database 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 generated TestHelpers file exposes several functions for testing your indexer:

MockDb Methods

> createMockDb

Creates an empty instance of a mock database:

const mockDbInitial = MockDb.createMockDb();

> set

Adds or updates an entity in the mock database:

const updatedMockDb = mockDbInitial.entities.EntityName.set(entity);

Where EntityName is the entity defined in your Schema

> get

Retrieves an entity from the mock database by its ID:

const entity = updatedMockDb.entities.EntityName.get(id);

Where EntityName is the entity defined in your Schema

Event Methods

> createMockEvent

Creates a mock blockchain event with the parameters you specify:

const eventMock = ContractName.EventName.createMockEvent({ params });

Where ContactName is the name of the contract defined in the config

Where EventName is the name of the event being emitted

Where params is an object of the parameters emitted in the event

You can optionally specify detailed event metadata using the mockEventData parameter:

{
chainId,
srcAddress,
logIndex,
block: {
number,
timestamp,
hash,
// and the following optionally based on field selection:
parentHash, nonce, sha3Uncles, logsBloom, transactionsRoot, stateRoot, receiptsRoot,
miner, difficulty, totalDifficulty, extraData, size, gasLimit, gasUsed, uncles,
baseFeePerGas, blobGasUsed, excessBlobGas, parentBeaconBlockRoot, withdrawalsRoot,
l1BlockNumber, sendCount, sendRoot, mixHash
},
transaction: {
// This is empty by default - but can have the following based on field selection:
number, timestamp, hash, parentHash, nonce, sha3Uncles, logsBloom, transactionsRoot,
stateRoot, receiptsRoot, miner, difficulty, totalDifficulty, extraData, size, gasLimit,
gasUsed, uncles, baseFeePerGas, blobGasUsed, excessBlobGas, parentBeaconBlockRoot,
withdrawalsRoot, l1BlockNumber, sendCount, sendRoot, mixHash,
}
}

Please see field_selection section in config file on how to customize available block and transaction fields.

> processEvent

Processes a mock event through your event handler and returns the updated mock database:

const updatedMockDbFromEvent = await ContractName.EventName.processEvent({
event: eventMock,
mockDb: updatedMockDb,
});

Where ContactName is the name of the contract defined in the config

Where EventName is the name of the event being emitted

Assertions

The testing library works with any JavaScript assertion library. In the examples, we use Node.js's built-in assert module, but you can also use popular alternatives like chai or expect.

Common assertion patterns include:

  • assert.deepEqual(expectedEntity, actualEntity) - Check that entire entities match
  • assert.equal(expectedValue, actualEntity.property) - Verify specific property values
  • assert.ok(updatedMockDb.entities.Entity.get(id)) - 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:

it("A NewGreeting event creates a User entity", async () => {
// Step 1: Initialize an empty mock database
const mockDbInitial = MockDb.createMockDb();

// Step 2: Define test data
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";

// Step 3: Create a mock event with our test data
const mockNewGreetingEvent = Greeter.NewGreeting.createMockEvent({
greeting: greeting,
user: userAddress,
});

// Step 4: Process the event through the handler
const updatedMockDb = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
});

// Step 5: Define what we expect to see in the database
const expectedUserEntity = {
id: userAddress,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
};

// Step 6: Verify the database contains what we expect
const actualUserEntity = updatedMockDb.entities.User.get(userAddress);
assert.deepEqual(expectedUserEntity, actualUserEntity);
});

Testing Entity Updates: 2 Greetings from the Same User

This example tests that when the same user sends multiple greetings, the counter increments correctly:

it("2 Greetings from the same users results in that user having a greeter count of 2", async () => {
// Step 1: Initialize the mock database
const mockDbInitial = MockDb.createMockDb();

// Step 2: Define test data for two events
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";
const greetingAgain = "Oh hello again";

// Step 3: Create the first mock event
const mockNewGreetingEvent = Greeter.NewGreeting.createMockEvent({
greeting: greeting,
user: userAddress,
});

// Step 4: Create the second mock event
const mockNewGreetingEvent2 = Greeter.NewGreeting.createMockEvent({
greeting: greetingAgain,
user: userAddress,
});

// Step 5: Process the first event
const updatedMockDb = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
});

// Step 6: Process the second event with the updated database
const updatedMockDb2 = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent2,
mockDb: updatedMockDb,
});

// Step 7: Get the entity from the mock database
const actualUserEntity = updatedMockDb2.entities.User.get(userAddress);

// Step 8: Verify the greeting count is 2
assert.equal(2, actualUserEntity?.numberOfGreetings);
});

Troubleshooting

If you encounter issues with your tests, check the following:

Environment and Setup

  1. Verify your Envio version: The testing library is available in versions v0.0.26 and above

    envio -V
  2. Ensure you've generated testing code: Always run codegen after updating your schema or config

    pnpm codegen
  3. Check your imports: Make sure you're importing the correct files

const assert = require("assert");
const { MockDb, Greeter } = require("../generated/src/TestHelpers.bs");
const { Addresses } = require("../generated/src/bindings/Ethers.bs");

Common Issues and Solutions

  • "Cannot read properties of undefined": This usually means an entity wasn't found in the database. Verify your IDs match exactly and that the entity exists before accessing it.

  • "Type mismatch": Ensure that your entity structure matches what's defined in your schema. Type issues are common when working with numeric types (like BigInt vs number).

  • ReScript specific setup: If using ReScript, remember to update your bsconfig.json file:

    {
    "sources": [
    { "dir": "src", "subdirs": true },
    { "dir": "test", "subdirs": true }
    ],
    "bs-dependencies": ["rescript-mocha"]
    }
  • Debug database state: If you're having trouble with assertions, add a debug log to see the exact state of your entities:

    console.log(
    JSON.stringify(updatedMockDb.entities.User.get(userAddress), null, 2)
    );

If you encounter any issues or have questions, please reach out to us on Discord