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:
- Generate
greeter
template in TypeScript using Envio CLI
pnpx envio init template -l typescript -d greeter -t greeter -n greeter
- Run tests
pnpm test
- 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
- Install your preferred testing framework, for example Mocha
- Javascript
- Typescript
- Rescript
pnpm i mocha
pnpm i mocha @types/mocha
pnpm i mocha rescript-mocha
Make sure to update your bsconfig
file to add the test folder as a source
and add rescript-mocha
as a bs-dependency
-
Create a
test
folder and a test file (e.g.,test.js
) inside it -
Add a test command to your
package.json
"test": "mocha",
- 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:
- Initialize the mock database (empty or with predefined entities)
- Create a mock event with test parameters
- Process the mock event through your handler(s)
- 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 matchassert.equal(expectedValue, actualEntity.property)
- Verify specific property valuesassert.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:
- Javascript
- Typescript
- Rescript
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);
});
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: UserEntity = {
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);
});
it("A NewGreeting event creates a User entity", async () => {
// Step 1: Initialize an empty mock database
let mockDbInitial = TestHelpers.MockDb.createMockDb()
// Step 2: Define test data
let userAddress = Ethers.Addresses.defaultAddress
let greeting = "Hi there"
// Step 3: Create a mock event with our test data
let mockNewGreetingEvent = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting,
user: userAddress,
})
// Step 4: Process the event through the handler
let updatedMockDb = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
})
// Step 5: Define what we expect to see in the database
let expectedUserEntity: Types.userEntity = {
id: userAddress->Ethers.ethAddressToString,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
}
// Step 6: Verify the database contains what we expect
let actualUserEntity =
updatedMockDb.entities.user.get(userAddress->Ethers.ethAddressToString)->Option.getExn
Assert.deep_equal(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:
- Javascript
- Typescript
- Rescript
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);
});
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);
});
it("2 Greetings from the same users results in that user having a greeter count of 2", async () => {
// Step 1: Initialize the mock database
let mockDbInitial = TestHelpers.MockDb.createMockDb()
// Step 2: Define test data for two events
let userAddress = Ethers.Addresses.defaultAddress
let greeting = "Hi there"
let greetingAgain = "Oh hello again"
// Step 3: Create the first mock event
let mockNewGreetingEvent = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting,
user: userAddress,
})
// Step 4: Create the second mock event
let mockNewGreetingEvent2 = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting: greetingAgain,
user: userAddress,
})
// Step 5: Process the first event
let updatedMockDb = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
})
// Step 6: Process the second event with the updated database
let updatedMockDb2 = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent2,
mockDb: updatedMockDb,
})
// Step 7: Set our expected value
let expectedGreetingCount = 2
// Step 8: Get the entity from the mock database
let actualUserEntity =
updatedMockDb2.entities.user.get(userAddress->Ethers.ethAddressToString)->Option.getExn
// Step 9: Verify the greeting count is 2
Assert.equal(actualUserEntity.numberOfGreetings, expectedGreetingCount)
})
Troubleshooting
If you encounter issues with your tests, check the following:
Environment and Setup
-
Verify your Envio version: The testing library is available in versions
v0.0.26
and aboveenvio -V
-
Ensure you've generated testing code: Always run codegen after updating your schema or config
pnpm codegen
-
Check your imports: Make sure you're importing the correct files
- Javascript
- Typescript
- Rescript
const assert = require("assert");
const { MockDb, Greeter } = require("../generated/src/TestHelpers.bs");
const { Addresses } = require("../generated/src/bindings/Ethers.bs");
import assert from "assert";
import { MockDb, Greeter } from "../generated/src/TestHelpers.gen";
import { UserEntity } from "../generated/src/Types.gen";
import { Addresses } from "../generated/src/bindings/Ethers.gen";
open RescriptMocha
open Mocha
open Belt
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
vsnumber
). -
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