Skip to main content
Version: v2

Testing

Introduction

Envio comes with a built-in testing library to assist developers write tests for their indexer. This library is specifically crafted to mock database states as well as events and assert event handler logic.

The testing library is simply a series of helper functions that can be used to write tests, which means that any JavaScript-based testing framework can be used. In the examples below, we use Mocha.

Learn by doing

Tests are written in JavaScript, TypeScript, and ReScript for the Greeter template and can be explored by following the steps below

Generate greeter template in TypeScript using Envio CLI

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 page assumes you have a functioning indexer setup for which you intend to write tests.

Install preferred testing framework, for example Mocha

pnpm i mocha

Validate you're using Envio v0.0.26 or above

envio -V

Create a test folder and a test file test.js inside it

Add a test command to your package.json

"test": "mocha",

Make sure to run envio codegen when changes are made to the config or schema files to regenerate the testing library (TestHelpers file).

envio codegen

Writing tests

Test library design

The library is designed to allow developers to mock the database, mock events and assert different outcomes to the database from these events.

The mock database is immutable and each action returns a new instance of the mocked database. This makes it robust and easy to test against previous states of the database.

Example steps of what most tests will look like

  1. Initialize the mock database
  2. Create a mock event
  3. Process the mock event on the mock database
  4. Assert against the expected database state

API

The generated TestHelpers file exposes a few different functions:

> createMockDb

The ability to create an instance of the MockDb

const mockDbInitial = MockDb.createMockDb();

> set

The ability to set an entity of a MockDb

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

Where EntityName is the entity defined in the Schema

> get

  1. Ability to get an entity from the MockDb
const entity = updatedMockDb.entities.EntityName.get(id);

Where EntityName is the entity defined in the Schema

> createMockEvent

The ability to create an instance of an event

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

Users can also define specific event log details by using mockEventData object as a parameter inside params object. This is useful when you want to test against specific event log details like block number, transaction hash, etc.

The mockEventData object can include the following properties:

{
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

The ability to process an event on a mockDb

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

In the examples we use NodeJS's built-in assert module but you can use other popular JavaScript-based assertion libraries like chai or expect.

Examples

A NewGreeting event creates a User entity

it("A NewGreeting event creates a User entity", async () => {
// Initializing the mock database
const mockDbInitial = MockDb.createMockDb();

// Initializing values for mock event
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";

// Creating a mock event
const mockNewGreetingEvent = Greeter.NewGreeting.createMockEvent({
greeting: greeting,
user: userAddress,
});

// Processing the mock event on the mock database
const updatedMockDb = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
});

// Expected entity that should be created
const expectedUserEntity = {
id: userAddress,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
};

// Getting the entity from the mock database
const actualUserEntity = updatedMockDb.entities.User.get(userAddress);

// Asserting that the entity in the mock database is the same as the expected entity
assert.deepEqual(expectedUserEntity, actualUserEntity);
});

2 Greetings from the same users results in that user having a greeter count of 2

it("2 Greetings from the same users results in that user having a greeter count of 2", async () => {
// Initializing the mock database
const mockDbInitial = MockDb.createMockDb();
// Initializing values for mock event
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";
const greetingAgain = "Oh hello again";

// Creating a mock event
const mockNewGreetingEvent = Greeter.NewGreeting.createMockEvent({
greeting: greeting,
user: userAddress,
});

// Creating a mock event
const mockNewGreetingEvent2 = Greeter.NewGreeting.createMockEvent({
greeting: greetingAgain,
user: userAddress,
});

// Processing the mock event on the mock database
const updatedMockDb = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
});

// Processing the mock event on the updated mock database
const updatedMockDb2 = await Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent2,
mockDb: updatedMockDb,
});

// Getting the entity from the mock database
const actualUserEntity = updatedMockDb2.entities.User.get(userAddress);

// Asserting that the field value of the entity in the mock database is the same as the expected field value
assert.equal(2, actualUserEntity?.numberOfGreetings);
});

Troubleshooting

The testing code is available in versions of Envio v0.0.26 and above.

Make sure to import relevant files and packages into your test file, it might look something like this;

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

Dev note: 📢 When working in ReScript make sure to update your bsconfig file to add the test folder as a source and add rescript-mocha as a bs-dependency

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