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
- 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
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
- Initialize the mock database
- Create a mock event
- Process the mock event on the mock database
- 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
- 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
- Javascript
- Typescript
- Rescript
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);
});
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: UserEntity = {
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);
});
it("A NewGreeting event creates a User entity", async () => {
// Initializing the mock database
let mockDbInitial = TestHelpers.MockDb.createMockDb()
// Initializing values for mock event
let userAddress = Ethers.Addresses.defaultAddress
let greeting = "Hi there"
// Creating a mock event
let mockNewGreetingEvent = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting,
user: userAddress,
})
// Processing the mock event on the mock database
let updatedMockDb = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
})
// Expected entity that should be created
let expectedUserEntity: Types.userEntity = {
id: userAddress->Ethers.ethAddressToString,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
}
// Getting the entity from the mock database
let actualUserEntity =
updatedMockDb.entities.user.get(userAddress->Ethers.ethAddressToString)->Option.getExn
// Asserting that the entity in the mock database is the same as the expected entity
Assert.deep_equal(expectedUserEntity, actualUserEntity)
})
2 Greetings from the same users results in that user having a greeter count of 2
- Javascript
- Typescript
- Rescript
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);
});
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);
});
it("2 Greetings from the same users results in that user having a greeter count of 2", async () => {
// Initializing the mock database
let mockDbInitial = TestHelpers.MockDb.createMockDb()
// Initializing values for mock event
let userAddress = Ethers.Addresses.defaultAddress
let greeting = "Hi there"
let greetingAgain = "Oh hello again"
// Creating a mock event
let mockNewGreetingEvent = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting,
user: userAddress,
})
// Creating a mock event
let mockNewGreetingEvent2 = TestHelpers.Greeter.NewGreeting.createMockEvent({
greeting: greetingAgain,
user: userAddress,
})
// Processing the mock event on the mock database
let updatedMockDb = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent,
mockDb: mockDbInitial,
})
// Processing the mock event on the updated mock database
let updatedMockDb2 = await TestHelpers.Greeter.NewGreeting.processEvent({
event: mockNewGreetingEvent2,
mockDb: updatedMockDb,
})
let expectedGreetingCount = 2
// Getting the entity from the mock database
let actualUserEntity =
updatedMockDb2.entities.user.get(userAddress->Ethers.ethAddressToString)->Option.getExn
// Asserting that the field value of the entity in the mock database is the same as the expected field value
Assert.equal(actualUserEntity.numberOfGreetings, expectedGreetingCount)
})
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;
- 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
Dev note: 📢 When working in ReScript make sure to update your
bsconfig
file to add the test folder as a source and addrescript-mocha
as abs-dependency
If you encounter any issues or have questions, please reach out to us on Discord