> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

## 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

```bash
pnpx envio@2 init template -l typescript -d greeter -t greeter -n greeter
```

2. Run tests

```bash
pnpm test
```

3. 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](https://mochajs.org/)

<Tabs>
  <TabItem value="typescript" label="TypeScript">

```bash
pnpm i mocha @types/mocha
```

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```bash
pnpm i mocha
```

  </TabItem>
  <TabItem value="rescript" label="ReScript">

```bash
pnpm i mocha rescript-mocha
```

:::note
Make sure to update your `rescript.json` file to add the test folder as a source
and add `rescript-mocha` as a `bs-dependency`
:::

  </TabItem>
</Tabs>

2. Create a `test` folder and a test file (e.g., `test.js`) inside it

3. Add a test command to your `package.json`

```json
"test": "mocha",
```

4. Generate the testing library by running:

```bash
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:

```javascript
const mockDb = MockDb.createMockDb();
```

#### > `set`

Adds or updates an entity in the mock database:

```javascript
const updatedMockDb = mockDb.entities.EntityName.set(entity);
```

:::note
Where EntityName is the entity defined in your Schema
:::

#### > `get`

Retrieves an entity from the mock database by its ID:

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

:::note
Where EntityName is the entity defined in your Schema
:::

#### > `processEvents`

:::note
Available starting from `envio@2.24.0`
:::

Processes one or more mock events through your event handlers and returns the updated mock database. This is the recommended approach for processing events:

```javascript
const updatedMockDb = await mockDb.processEvents([eventMock1, eventMock2]);
```

This method allows you to process multiple events in sequence, emulating how handlers would run in a real indexing scenario.

### Event Methods

#### > `createMockEvent`

Creates a mock blockchain event with the parameters you specify:

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

:::note
Where `ContactName` is the name of the contract defined in the config
:::

:::note
Where `EventName` is the name of the event being emitted
:::

:::note
Where `params` is an object of the parameters emitted in the event
:::

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

```typescript
{
  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](configuration-file#field-selection) section in config file on how to customize available block and transaction fields.

#### > `processEvent`

:::warning Deprecated
This method is deprecated and will be removed in a future version. Use `mockDb.processEvents([eventMock])` instead.
:::

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

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

:::note
Where `ContactName` is the name of the contract defined in the config
:::

:::note
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](https://www.chaijs.com/) or [expect](https://github.com/Automattic/expect.js).

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:

<Tabs>
  <TabItem value="typescript" label="TypeScript">

```typescript
it("A NewGreeting event creates a User entity", async () => {
  // Step 1: Initialize an empty mock database
  const mockDb = 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 mockDb.processEvents([mockNewGreetingEvent]);

  // 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);
});
```

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```javascript
it("A NewGreeting event creates a User entity", async () => {
  // Step 1: Initialize an empty mock database
  const mockDb = 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 mockDb.processEvents([mockNewGreetingEvent]);

  // 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);
});
```

  </TabItem>
  <TabItem value="rescript" label="ReScript">

```rescript
it("A NewGreeting event creates a User entity", async () => {
    // Step 1: Initialize an empty mock database
    let mockDb = 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 mockDb.processEvents([mockNewGreetingEvent])

    // 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)
  })
```

  </TabItem>
</Tabs>

### Testing Entity Updates: 2 Greetings from the Same User

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

<Tabs>
  <TabItem value="typescript" label="TypeScript">

```typescript
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 mockDb = 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 both events through the handlers
  const updatedMockDb = await mockDb.processEvents([
    mockNewGreetingEvent,
    mockNewGreetingEvent2,
  ]);

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

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

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```javascript
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 mockDb = 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 both events through the handlers
  const updatedMockDb = await mockDb.processEvents([
    mockNewGreetingEvent,
    mockNewGreetingEvent2,
  ]);

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

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

  </TabItem>
  <TabItem value="rescript" label="ReScript">

```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
    let mockDb = 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 both events through the handlers
    let updatedMockDb = await mockDb.processEvents([mockNewGreetingEvent, mockNewGreetingEvent2])

    // Step 6: Set our expected value
    let expectedGreetingCount = 2

    // Step 7: Get the entity from the mock database
    let actualUserEntity =
      updatedMockDb.entities.user.get(userAddress->Ethers.ethAddressToString)->Option.getExn

    // Step 8: Verify the greeting count is 2
    Assert.equal(actualUserEntity.numberOfGreetings, expectedGreetingCount)
  })
```

  </TabItem>
</Tabs>

---

## 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

   ```bash
   pnpm envio -v
   ```

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

   ```bash
   pnpm codegen
   ```

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

<Tabs>
   <TabItem value="typescript" label="TypeScript">

```typescript
import assert from "assert";
import { UserEntity, TestHelpers } from "generated";
const { MockDb, Greeter, Addresses } = TestHelpers;
```

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```javascript
const assert = require("assert");
const { UserEntity, TestHelpers } = require("generated");
const { MockDb, Greeter, Addresses } = TestHelpers;
```

  </TabItem>
  <TabItem value="rescript" label="ReScript">

```rescript
open RescriptMocha
open Mocha
open Belt
```

  </TabItem>
</Tabs>

### 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 `rescript.json` file:

  ```json
  {
    "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:
  ```javascript
  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](https://discord.gg/envio)
