Skip to main content

Unit Testing Framework

Matchstick is a unit testing framework, developed by LimeChain, that enables subgraph developers to test their mapping logic in a sandboxed environment and deploy their subgraphs with confidence!

Follow the Matchstick installation guide to install. Now, you can move on to writing your first unit test.

Write a Unit Test

Let's see how a simple unit test would look like, using the Gravatar Example Subgraph.

Assuming we have the following handler function (along with two helper functions to make our life easier):

export function handleNewGravatar(event: NewGravatar): void {
let gravatar = new Gravatar(event.params.id.toHex())
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}

export function handleNewGravatars(events: NewGravatar[]): void {
events.forEach((event) => {
handleNewGravatar(event)
})
}

export function createNewGravatarEvent(
id: i32,
ownerAddress: string,
displayName: string,
imageUrl: string
): NewGravatar {
let mockEvent = newMockEvent()
let newGravatarEvent = new NewGravatar(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
mockEvent.parameters
)
newGravatarEvent.parameters = new Array()
let idParam = new ethereum.EventParam('id', ethereum.Value.fromI32(id))
let addressParam = new ethereum.EventParam(
'ownderAddress',
ethereum.Value.fromAddress(Address.fromString(ownerAddress))
)
let displayNameParam = new ethereum.EventParam('displayName', ethereum.Value.fromString(displayName))
let imageUrlParam = new ethereum.EventParam('imageUrl', ethereum.Value.fromString(imageUrl))

newGravatarEvent.parameters.push(idParam)
newGravatarEvent.parameters.push(addressParam)
newGravatarEvent.parameters.push(displayNameParam)
newGravatarEvent.parameters.push(imageUrlParam)

return newGravatarEvent
}

We first have to create a test file in our project. We have chosen the name gravity.test.ts. In the newly created file we need to define a function named runTests(). It is important that the function has that exact name. This is an example of how our tests might look like:

import { clearStore, test, assert } from 'matchstick-as/assembly/index'
import { Gravatar } from '../../generated/schema'
import { NewGravatar } from '../../generated/Gravity/Gravity'
import { createNewGravatarEvent, handleNewGravatars } from '../mappings/gravity'

export function runTests(): void {
test('Can call mappings with custom events', () => {
// Initialise
let gravatar = new Gravatar('gravatarId0')
gravatar.save()

// Call mappings
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')

let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')

handleNewGravatars([newGravatarEvent, anotherGravatarEvent])

assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')

clearStore()
})

test('Next test', () => {
//...
})
}

That's a lot to unpack! First off, an important thing to notice is that we're importing things from matchstick-as, our AssemblyScript helper library (distributed as an npm module). You can find the repository here. matchstick-as provides us with useful testing methods and also defines the test() function which we will use to build our test blocks. The rest of it is pretty straightforward - here's what happens:

  • We're setting up our initial state and adding one custom Gravatar entity;
  • We define two NewGravatar event objects along with their data, using the createNewGravatarEvent() function;
  • We're calling out handler methods for those events - handleNewGravatars() and passing in the list of our custom events;
  • We assert the state of the store. How does that work? - We're passing a unique combination of Entity type and id. Then we check a specific field on that Entity and assert that it has the value we expect it to have. We're doing this both for the initial Gravatar Entity we added to the store, as well as the two Gravatar entities that gets added when the handler function is called;
  • And lastly - we're cleaning the store using clearStore() so that our next test can start with a fresh and empty store object. We can define as many test blocks as we want.

There we go - we've created our first test! ๐Ÿ‘

โ— IMPORTANT: In order for the tests to work, we need to export the runTests() function in our mappings file. It won't be used there, but the export statement has to be there so that it can get picked up by Rust later when running the tests.

You can export the tests wrapper function in your mappings file like this:

export { runTests } from "../tests/gravity.test.ts";

โ— IMPORTANT: Currently there's an issue with using Matchstick when deploying your subgraph. Please only use Matchstick for local testing, and remove/comment out this line (export { runTests } from "../tests/gravity.test.ts") once you're done. We expect to resolve this issue shortly, sorry for the inconvenience!

If you don't remove that line, you will get the following error message when attempting to deploy your subgraph:

/...
Mapping terminated before handling trigger: oneshot canceled
.../

Now in order to run our tests you simply need to run the following in your subgraph root folder:

graph test Gravity

And if all goes well you should be greeted with the following:

screenshot

Common test scenarios

Hydrating the store with a certain state

Users are able to hydrate the store with a known set of entities. Here's an example to initialise the store with a Gravatar entity:

let gravatar = new Gravatar('entryId')
gravatar.save()

Calling a mapping function with an event

A user can create a custom event and pass it to a mapping function that is bound to the store:

import { store } from 'matchstick-as/assembly/store'
import { NewGravatar } from '../../generated/Gravity/Gravity'
import { handleNewGravatars, createNewGravatarEvent } from './mapping'

let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')

handleNewGravatar(newGravatarEvent)

Calling all of the mappings with event fixtures

Users can call the mappings with test fixtures.

import { NewGravatar } from '../../generated/Gravity/Gravity'
import { store } from 'matchstick-as/assembly/store'
import { handleNewGravatars, createNewGravatarEvent } from './mapping'

let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')

let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')

handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
export function handleNewGravatars(events: NewGravatar[]): void {
events.forEach(event => {
handleNewGravatar(event);
});
}

Mocking contract calls

Users can mock contract calls:

import { addMetadata, assert, createMockedFunction, clearStore, test } from 'matchstick-as/assembly/index'
import { Gravity } from '../../generated/Gravity/Gravity'
import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts'

let contractAddress = Address.fromString('0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
let expectedResult = Address.fromString('0x90cBa2Bbb19ecc291A12066Fd8329D65FA1f1947')
let bigIntParam = BigInt.fromString('1234')
createMockedFunction(contractAddress, 'gravatarToOwner', 'gravatarToOwner(uint256):(address)')
.withArgs([ethereum.Value.fromSignedBigInt(bigIntParam)])
.returns([ethereum.Value.fromAddress(Address.fromString('0x90cBa2Bbb19ecc291A12066Fd8329D65FA1f1947'))])

let gravity = Gravity.bind(contractAddress)
let result = gravity.gravatarToOwner(bigIntParam)

assert.equals(ethereum.Value.fromAddress(expectedResult), ethereum.Value.fromAddress(result))

As demonstrated, in order to mock a contract call and hardcore a return value, the user must provide a contract address, function name, function signature, an array of arguments, and of course - the return value.

Users can also mock function reverts:

let contractAddress = Address.fromString('0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
createMockedFunction(contractAddress, 'getGravatar', 'getGravatar(address):(string,string)')
.withArgs([ethereum.Value.fromAddress(contractAddress)])
.reverts()

Asserting the state of the store

Users are able to assert the final (or midway) state of the store through asserting entities. In order to do this, the user has to supply an Entity type, the specific ID of an Entity, a name of a field on that Entity, and the expected value of the field. Here's a quick example:

import { assert } from 'matchstick-as/assembly/index'
import { Gravatar } from '../generated/schema'

let gravatar = new Gravatar('gravatarId0')
gravatar.save()

assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')

Running the assert.fieldEquals() function will check for equality of the given field against the given expected value. The test will fail and an error message will be outputted if the values are NOT equal. Otherwise the test will pass successfully.

Interacting with Event metadata

Users can use default transaction metadata, which could be returned as an ethereum.Event by using the newMockEvent() function. The following example shows how you can read/write to those fields on the Event object:

// Read
let logType = newGravatarEvent.logType

// Write
let UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'
newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)

Asserting variable equality

assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

Asserting that an Entity is not in the store

Users can assert that an entity does not exist in the store. The function takes an entity type and an id. If the entity is in fact in the store, the test will fail with a relevant error message. Here's a quick example of how to use this functionality:

assert.notInStore('Gravatar', '23')

Test run time duration in the log output

The log output includes the test run duration. Here's an example:

Jul 09 14:54:42.420 INFO Program execution time: 10.06022ms

Feedback

If you have any questions, feedback, feature requests or just want to reach out, the best place would be The Graph Discord where we have a dedicated channel for Matchstick, called ๐Ÿ”ฅ| unit-testing.