Desarrollando > Marco de Unit Testing

Marco de Unit Testing

Reading time: 26 min

¡Matchstick es un marco de unit testing, desarrollado por LimeChain, que permite a los developers de subgrafos probar su lógica de mapeo en un entorno sandbox y deployar sus subgrafos con confianza!

Empezando

Enlace a esta sección

Instalar dependencias

Enlace a esta sección

Para utilizar los métodos auxiliares de prueba y ejecutar las pruebas, deberás instalar las siguientes dependencias:

yarn add --dev matchstick-as

graph-node depende de PostgreSQL, por lo que si aún no lo tienes, deberás instalarlo. ¡Recomendamos ampliamente usar los comandos a continuación, ya que agregarlos de otra manera puede causar errores inesperados!

Comando de instalación de Postgres:

brew install postgresql

Crea un symlynk al último libpq.5.lib Es posible que primero debas crear este directorio /usr/local/opt/postgresql/lib/

ln -sf /usr/local/opt/postgresql@14/lib/postgresql@14/libpq.5.dylib /usr/local/opt/postgresql/lib/libpq.5.dylib

Comando de instalación de Postgres (depende de tu distribución):

sudo apt install postgresql

WSL (Subsistema de Windows para Linux)

Enlace a esta sección

Puedes usar Matchstick en WSL tanto con el enfoque de Docker como con el enfoque binario. Ya que WSL puede ser un poco complicado, aquí hay algunos consejos en caso de que encuentres problemas como

static BYTES = Symbol("Bytes") SyntaxError: Unexpected token =

o

<PROJECT_PATH>/node_modules/gluegun/build/index.js:13 throw up;

Asegúrate de tener una versión más reciente de Node.js, graph-cli ya no es compatible con v10.19.0, y esa sigue siendo la versión predeterminada para las nuevas imágenes de Ubuntu en WSL. Por ejemplo, se ha confirmado que Matchstick funciona en WSL con v18.1.0, puedes cambiar a él a través de nvm o si actualizas su Node.js global. ¡No olvides eliminar node_modules y ejecutar npm install nuevamente después de actualizar sus nodejs! Luego, asegúrate de tener libpq instalado, puedes hacerlo ejecutando

sudo apt-get install libpq-dev

Y finalmente, no uses graph test (que usa tu instalación global de graph-cli y por alguna razón parece que no funciona en WSL actualmente), en su lugar usa yarn test o npm run test (que usará la instancia local a nivel de proyecto de graph-cli, que funciona de maravilla). Para eso, por supuesto, necesitarías tener un script "test" en tu archivo package.json que puede ser algo tan simple como

{
"name": "demo-subgraph",
"version": "0.1.0",
"scripts": {
"test": "graph test",
...
},
"dependencies": {
"@graphprotocol/graph-cli": "^0.56.0",
"@graphprotocol/graph-ts": "^0.31.0",
"matchstick-as": "^0.6.0"
}
}

Para usar Matchstick en tu proyecto de subgrafo simplemente abre una terminal, navega a la carpeta raíz de tu proyecto y simplemente ejecuta graph test [options] <datasource> : descarga el binario Matchstick más reciente y ejecuta la prueba especificada o todas las pruebas en una carpeta de prueba (o todas las pruebas existentes si no se especifica un indicador de fuente de datos).

Esto ejecutará todas las pruebas en la carpeta de prueba:

graph test

Esto ejecutará una prueba llamada gravity.test.ts y/o todas las pruebas dentro de una carpeta llamada gravity:

graph test gravity

Esto ejecutará solo ese archivo de prueba específico:

graph test path/to/file.test.ts

Opciones:

-c, --coverage: Ejecuta las pruebas en modo de cobertura.
-d, --docker: Ejecuta las pruebas en un contenedor Docker (Nota: Por favor, ejecuta desde la carpeta raíz del subgrafo).
-f, --force: Binario: Vuelve a descargar el binario. Docker: Vuelve a descargar el archivo Docker y reconstruye la imagen Docker.
-h, --help: Muestra información de uso.
-l, --logs: Registra en la consola información sobre el sistema operativo, modelo de CPU y URL de descarga (para fines de depuración).
-r, --recompile: Fuerza a que las pruebas se recompilen.
-v, --version <tag>: Elije la versión del binario de Rust que deseas descargar/utilizar

Desde graph-cli 0.25.2, el comando graph test admite la ejecución de matchstick en un contenedor de Docker con la marca -d. La implementación de Docker utiliza bind-mount para que no tenga que reconstruir la imagen del Docker cada vez que se ejecuta el comando graph test -d. Como alternativa, puedes seguir las instrucciones del repositorio matchstick para ejecutar docker manualmente.

graph test -d forces docker run to run with flag -t. This must be removed to run inside non-interactive environments (like GitHub CI).

❗ Si ejecutaste previamente graph test, es posible que encuentres el siguiente error durante la compilación de docker:

error from sender: failed to xattr node_modules/binary-install-raw/bin/binary-<platform>: permission denied

En este caso, crea un .dockerignore en la carpeta raíz y agrega node_modules/binary-install-raw/bin

Configuración

Enlace a esta sección

Matchstick se puede configurar para usar pruebas personalizadas, bibliotecas y una ruta de manifesto personalizada a través del archivo de configuración matchstick.yaml:

testsFolder: path/to/tests
libsFolder: path/to/libs
manifestPath: path/to/subgraph.yaml

Subgrafo de demostración

Enlace a esta sección

Puedes probar y jugar con los ejemplos de esta guía clonando el Demo Subgraph repo

Tutoriales en vídeo

Enlace a esta sección

También puedes ver la serie de videos en "How to use Matchstick to write unit tests for your subgraphs"

Tests structure

Enlace a esta sección

IMPORTANT: The test structure described below depens on matchstick-as version >=0.5.0

describe(name: String , () => {}) - Defines a test group.

Notas:

  • Las descripciones no son obligatorias. Todavía puedes usar test() a la antigua usanza, fuera de los bloques describe()

Ejemplo:

import { describe, test } from "matchstick-as/assembly/index"
import { handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar()", () => {
test("Should create a new Gravatar entity", () => {
...
})
})

Ejemplo anidado de describe():

import { describe, test } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar } from "../../src/gravity"
describe("handleUpdatedGravatar()", () => {
describe("When entity exists", () => {
test("updates the entity", () => {
...
})
})
describe("When entity does not exists", () => {
test("it creates a new entity", () => {
...
})
})
})

test(name: String, () =>, should_fail: bool) - Define un caso de prueba. Puedes usar test() dentro de los bloques describe() o de forma independiente.

Ejemplo:

import { describe, test } from "matchstick-as/assembly/index"
import { handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar()", () => {
test("Should create a new Entity", () => {
...
})
})

o

test("handleNewGravatar() should create a new entity", () => {
...
})

Ejecuta un bloque de código antes de cualquiera de las pruebas del archivo. Si beforeAll se declara dentro de un bloque describe, se ejecuta al principio de ese bloque describe.

Ejemplos:

El código dentro de afterAll se ejecutará una vez después de todas las pruebas en el archivo.

import { describe, test, beforeAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { Gravatar } from "../../generated/schema"
beforeAll(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = “First Gravatar”
gravatar.save()
...
})
describe("When the entity does not exist", () => {
test("it should create a new Gravatar with id 0x1", () => {
...
})
})
describe("When entity already exists", () => {
test("it should update the Gravatar with id 0x0", () => {
...
})
})

El código dentro de beforeAll se ejecutará una vez antes de todas las pruebas en el primer bloque describe

import { describe, test, beforeAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { Gravatar } from "../../generated/schema"
describe("handleUpdatedGravatar()", () => {
beforeAll(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = “First Gravatar”
gravatar.save()
...
})
test("updates Gravatar with id 0x0", () => {
...
})
test("creates new Gravatar with id 0x1", () => {
...
})
})

Ejecuta un bloque de código después de todas las pruebas del archivo. Si afterAll se declara dentro de un bloque describe, se ejecuta al final de ese bloque describe.

Ejemplo:

El código dentro de afterAll se ejecutará una vez después de all las pruebas en el archivo.

import { describe, test, afterAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { store } from "@graphprotocol/graph-ts"
afterAll(() => {
store.remove("Gravatar", "0x0")
...
})
describe("handleNewGravatar, () => {
test("creates Gravatar with id 0x0", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("updates Gravatar with id 0x0", () => {
...
})
})

El código dentro de afterAll se ejecutará una vez después de todas las pruebas en el primer bloque describe

import { describe, test, afterAll, clearStore } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar", () => {
afterAll(() => {
store.remove("Gravatar", "0x1")
...
})
test("It creates a new entity with Id 0x0", () => {
...
})
test("It creates a new entity with Id 0x1", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("updates Gravatar with id 0x0", () => {
...
})
})

Ejecuta un bloque de código antes de cada prueba. Si beforeEach se declara dentro de un bloque describe, se ejecuta antes de cada prueba en ese bloque describe.

Ejemplos: el código dentro de beforeEach se ejecutará antes de cada prueba.

import { describe, test, beforeEach, clearStore } from "matchstick-as/assembly/index"
import { handleNewGravatars } from "./utils"
beforeEach(() => {
clearStore() // <-- clear the store before each test in the file
})
describe("handleNewGravatars, () => {
test("A test that requires a clean store", () => {
...
})
test("Second that requires a clean store", () => {
...
})
})
...

El código dentro de beforeEach se ejecutará solo antes de cada prueba en el describe

import { describe, test, beforeEach } from 'matchstick-as/assembly/index'
import { handleUpdatedGravatar, handleNewGravatar } from '../../src/gravity'
describe('handleUpdatedGravatars', () => {
beforeEach(() => {
let gravatar = new Gravatar('0x0')
gravatar.displayName = 'First Gravatar'
gravatar.imageUrl = ''
gravatar.save()
})
test('Upates the displayName', () => {
assert.fieldEquals('Gravatar', '0x0', 'displayName', 'First Gravatar')
// code that should update the displayName to 1st Gravatar
assert.fieldEquals('Gravatar', '0x0', 'displayName', '1st Gravatar')
store.remove('Gravatar', '0x0')
})
test('Updates the imageUrl', () => {
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', '')
// code that should changes the imageUrl to https://www.gravatar.com/avatar/0x0
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', 'https://www.gravatar.com/avatar/0x0')
store.remove('Gravatar', '0x0')
})
})

Ejecuta un bloque de código después de cada prueba. Si afterEach se declara dentro de un bloque describe, se ejecuta después de cada prueba en ese bloque describe.

Ejemplos:

El código dentro de afterEach se ejecutará después de cada prueba.

import { describe, test, beforeEach, afterEach } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
beforeEach(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = “First Gravatar”
gravatar.save()
})
afterEach(() => {
store.remove("Gravatar", "0x0")
})
describe("handleNewGravatar", () => {
...
})
describe("handleUpdatedGravatar", () => {
test("Upates the displayName", () => {
assert.fieldEquals("Gravatar", "0x0", "displayName", "First Gravatar")
// code that should update the displayName to 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// code that should changes the imageUrl to https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})

El código dentro de afterEach se ejecutará después de cada prueba en ese describe

import { describe, test, beforeEach, afterEach } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar", () => {
...
})
describe("handleUpdatedGravatar", () => {
beforeEach(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = "First Gravatar"
gravatar.imageUrl = ""
gravatar.save()
})
afterEach(() => {
store.remove("Gravatar", "0x0")
})
test("Upates the displayName", () => {
assert.fieldEquals("Gravatar", "0x0", "displayName", "First Gravatar")
// code that should update the displayName to 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// code that should changes the imageUrl to https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})
fieldEquals(entityType: string, id: string, fieldName: string, expectedVal: string)
equals(expected: ethereum.Value, actual: ethereum.Value)
notInStore(entityType: string, id: string)
addressEquals(address1: Address, address2: Address)
bytesEquals(bytes1: Bytes, bytes2: Bytes)
i32Equals(number1: i32, number2: i32)
bigIntEquals(bigInt1: BigInt, bigInt2: BigInt)
booleanEquals(bool1: boolean, bool2: boolean)
stringEquals(string1: string, string2: string)
arrayEquals(array1: Array<ethereum.Value>, array2: Array<ethereum.Value>)
tupleEquals(tuple1: ethereum.Tuple, tuple2: ethereum.Tuple)
assertTrue(value: boolean)
assertNull<T>(value: T)
assertNotNull<T>(value: T)
entityCount(entityType: string, expectedCount: i32)

As of version 0.6.0, asserts support custom error messages as well

assert.fieldEquals('Gravatar', '0x123', 'id', '0x123', 'Id should be 0x123')
assert.equals(ethereum.Value.fromI32(1), ethereum.Value.fromI32(1), 'Value should equal 1')
assert.notInStore('Gravatar', '0x124', 'Gravatar should not be in store')
assert.addressEquals(Address.zero(), Address.zero(), 'Address should be zero')
assert.bytesEquals(Bytes.fromUTF8('0x123'), Bytes.fromUTF8('0x123'), 'Bytes should be equal')
assert.i32Equals(2, 2, 'I32 should equal 2')
assert.bigIntEquals(BigInt.fromI32(1), BigInt.fromI32(1), 'BigInt should equal 1')
assert.booleanEquals(true, true, 'Boolean should be true')
assert.stringEquals('1', '1', 'String should equal 1')
assert.arrayEquals([ethereum.Value.fromI32(1)], [ethereum.Value.fromI32(1)], 'Arrays should be equal')
assert.tupleEquals(
changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),
changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),
'Tuples should be equal',
)
assert.assertTrue(true, 'Should be true')
assert.assertNull(null, 'Should be null')
assert.assertNotNull('not null', 'Should be not null')
assert.entityCount('Gravatar', 1, 'There should be 2 gravatars')
assert.dataSourceCount('GraphTokenLockWallet', 1, 'GraphTokenLockWallet template should have one data source')
assert.dataSourceExists(
'GraphTokenLockWallet',
Address.zero().toHexString(),
'GraphTokenLockWallet should have a data source for zero address',
)

Escribir un Unit Test

Enlace a esta sección

Veamos cómo se vería un unit test simple usando los ejemplos de Gravatar en el Demo Subgraph.

Suponiendo que tenemos la siguiente función handler (junto con dos funciones auxiliares para hacernos la vida más fácil):

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
}

Primero tenemos que crear un archivo de prueba en nuestro proyecto. Este es un ejemplo de cómo podría verse:

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'
test('Can call mappings with custom events', () => {
// Create a test entity and save it in the store as initial state (optional)
let gravatar = new Gravatar('gravatarId0')
gravatar.save()
// Create mock events
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
// Call mapping functions passing the events we just created
handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
// Assert the state of the store
assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')
// Clear the store in order to start the next test off on a clean slate
clearStore()
})
test('Next test', () => {
//...
})

¡Eso es mucho para desempacar! En primer lugar, algo importante a tener en cuenta es que estamos importando elementos de matchstick-as, nuestra biblioteca auxiliar de AssemblyScript (distribuida como un módulo npm). Puede encontrar el repositorio aquí. matchstick-as nos proporciona métodos de prueba útiles y también define la función test() que usaremos para construir nuestros bloques de prueba. El resto es bastante sencillo: esto es lo que sucede:

  • Estamos configurando nuestro estado inicial y agregando una entidad Gravatar personalizada;
  • Definimos dos objetos de evento NewGravatar junto con sus datos, usando la función createNewGravatarEvent();
  • Estamos llamando a los métodos handler para esos eventos - handleNewGravatars() y pasando la lista de nuestros eventos personalizados;
  • Hacemos valer el estado del almacén. ¿Cómo funciona eso? - Pasamos una combinación única de tipo de Entidad e id. A continuación, comprobamos un campo específico de esa Entidad y afirmamos que tiene el valor que esperamos que tenga. Hacemos esto tanto para la Entidad Gravatar inicial que añadimos al almacén, como para las dos entidades Gravatar que se añaden cuando se llama a la función del handler;
  • Y, por último, estamos limpiando el store usando clearStore() para que nuestra próxima prueba pueda comenzar con un objeto store nuevo y vacío. Podemos definir tantos bloques de prueba como queramos.

Ahí vamos: ¡hemos creado nuestra primera prueba! 👏

Ahora, para ejecutar nuestras pruebas, simplemente necesitas ejecutar lo siguiente en la carpeta raíz de tu subgrafo:

prueba graph Gravity

Y si todo va bien, deberías ser recibido con lo siguiente:

Matchstick diciendo "¡Todas las pruebas pasaron!"

Escenarios de prueba comunes

Enlace a esta sección

Abastecer la tienda con un cierto estado

Enlace a esta sección

Los usuarios pueden abastecer la tienda con un conjunto conocido de entidades. Aquí hay un ejemplo para inicializar la tienda con una entidad Gravatar:

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

Llamar a una función de mapeo con un evento

Enlace a esta sección

Un usuario puede crear un evento personalizado y pasarlo a una función de mapeo que está vinculada al 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)

Llamar a todos los mapeos con fixtures de eventos

Enlace a esta sección

Los usuarios pueden llamar a los mapeos con fixtures de prueba.

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 de llamadas de contrato

Enlace a esta sección

Los usuarios pueden mock llamadas de contrato:

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

Como se demostró, para hacer mock de una llamada de contrato y obtener un valor de retorno, el usuario debe proporcionar una dirección de contrato, un nombre de función, una firma de función, una serie de argumentos y, por supuesto, el valor de retorno.

Las usuarios también pueden hacer mock de reversiones de funciones:

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

Mocking de archivos IPFS (desde matchstick 0.4.1)

Enlace a esta sección

Los usuarios pueden mock archivos IPFS usando la función mockIpfsFile(hash, filePath). La función acepta dos argumentos, el primero es el hash/ruta del archivo IPFS y el segundo es la ruta a un archivo local.

NOTA: Al probar ipfs.map/ipfs.mapJSON, la función callback debe exportarse desde el archivo de prueba para que matchstck la detecte, como la función processGravatar() en el siguiente ejemplo de prueba:

archivo .test.ts:

import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'
import { ipfs } from '@graphprotocol/graph-ts'
import { gravatarFromIpfs } from './utils'
// Export ipfs.map() callback in order for matchstck to detect it
export { processGravatar } from './utils'
test('ipfs.cat', () => {
mockIpfsFile('ipfsCatfileHash', 'tests/ipfs/cat.json')
assert.entityCount(GRAVATAR_ENTITY_TYPE, 0)
gravatarFromIpfs()
assert.entityCount(GRAVATAR_ENTITY_TYPE, 1)
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '1', 'imageUrl', 'https://i.ytimg.com/vi/MELP46s8Cic/maxresdefault.jpg')
clearStore()
})
test('ipfs.map', () => {
mockIpfsFile('ipfsMapfileHash', 'tests/ipfs/map.json')
assert.entityCount(GRAVATAR_ENTITY_TYPE, 0)
ipfs.map('ipfsMapfileHash', 'processGravatar', Value.fromString('Gravatar'), ['json'])
assert.entityCount(GRAVATAR_ENTITY_TYPE, 3)
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '1', 'displayName', 'Gravatar1')
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '2', 'displayName', 'Gravatar2')
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '3', 'displayName', 'Gravatar3')
})

archivo utils.ts:

import { Address, ethereum, JSONValue, Value, ipfs, json, Bytes } from "@graphprotocol/graph-ts"
import { Gravatar } from "../../generated/schema"
...
// ipfs.map callback
export function processGravatar(value: JSONValue, userData: Value): void {
// See the JSONValue documentation for details on dealing
// with JSON values
let obj = value.toObject()
let id = obj.get('id')
if (!id) {
return
}
// Callbacks can also created entities
let gravatar = new Gravatar(id.toString())
gravatar.displayName = userData.toString() + id.toString()
gravatar.save()
}
// function that calls ipfs.cat
export function gravatarFromIpfs(): void {
let rawData = ipfs.cat("ipfsCatfileHash")
if (!rawData) {
return
}
let jsonData = json.fromBytes(rawData as Bytes).toObject()
let id = jsonData.get('id')
let url = jsonData.get("imageUrl")
if (!id || !url) {
return
}
let gravatar = new Gravatar(id.toString())
gravatar.imageUrl = url.toString()
gravatar.save()
}

Afirmando el estado del almacenamiento

Enlace a esta sección

Los usuarios pueden afirmar el estado final (o intermedio) del almacenamiento mediante la afirmación de entidades. Para hacerlo, el usuario debe proporcionar un tipo de entidad, el ID específico de una entidad, un nombre de campo en esa entidad y el valor esperado del campo. Aquí hay un ejemplo rápido:

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')

Ejecutar la función assert.fieldEquals() verificará la igualdad del campo dado contra el valor esperado dado. La prueba fallará y se generará un mensaje de error si los valores NO son iguales. De lo contrario, la prueba pasará con éxito.

Interactuar con metadatos de eventos

Enlace a esta sección

Los usuarios pueden usar metadatos de transacción predeterminados, que podrían devolverse como un evento de ethereum mediante el uso de la función newMockEvent(). El siguiente ejemplo muestra cómo puedes leer/escribir en esos campos en el objeto Event:

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

Afirmar la igualdad de variables

Enlace a esta sección
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

Afirmar que una Entidad no está en el almacenamiento

Enlace a esta sección

Los usuarios pueden afirmar que una entidad no existe en el almacenamiento. La función toma un tipo de entidad y una identificación. Si la entidad está de hecho en el almacenamiento, la prueba fallará con un mensaje de error relevante. Aquí hay un ejemplo rápido de cómo usar esta funcionalidad:

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

Printing the whole store, or single entities from it (for debug purposes)

Enlace a esta sección

Puede imprimir todo el almacenamiento a la consola usando esta función de ayuda:

import { logStore } from 'matchstick-as/assembly/store'
logStore()

As of version 0.6.0, logStore no longer prints derived fields, instead users can use the new logEntity function. Of course logEntity can be used to print any entity, not just ones that have derived fields. logEntity takes the entity type, entity id and a showRelated flag to indicate if users want to print the related derived entities.

import { logEntity } from 'matchstick-as/assembly/store'
logEntity("Gravatar", 23, true)

Fallo esperado

Enlace a esta sección

Los usuarios pueden tener fallas de prueba esperadas, usando el indicador shouldFail en las funciones test():

test(
'Should throw an error',
() => {
throw new Error()
},
true,
)

Si la prueba está marcada con shouldFail = true pero NO falla, aparecerá como un error en los registros y el bloque de prueba fallará. Además, si está marcado con shouldFail = false (el estado predeterminado), el ejecutor de prueba fallará.

Tener logs personalizados en los unit tests es exactamente lo mismo que hacer logging en los mapeos. La diferencia es que el objeto de registro debe importarse desde matchstick-as en lugar de graph-ts. Aquí hay un ejemplo simple con todos los tipos de log no críticos:

import { test } from "matchstick-as/assembly/index";
import { log } from "matchstick-as/assembly/log";
test("Success", () => {
log.success("Success!". []);
});
test("Error", () => {
log.error("Error :( ", []);
});
test("Debug", () => {
log.debug("Debugging...", []);
});
test("Info", () => {
log.info("Info!", []);
});
test("Warning", () => {
log.warning("Warning!", []);
});

Los usuarios también pueden simular una falla crítica, así:

test('Blow everything up', () => {
log.critical('Boom!')
})

El logging de errores críticos detendrá la ejecución de las pruebas y explotará todo. Después de todo, queremos asegurarnos de que tu código no tenga logs críticos en el deployment, y deberías darte cuenta de inmediato si eso sucediera.

Testing de campos derivados

Enlace a esta sección

Testing derived fields is a feature which allows users to set a field on a certain entity and have another entity be updated automatically if it derives one of its fields from the first entity.

Before version 0.6.0 it was possible to get the derived entities by accessing them as entity fields/properties, like so:

let entity = ExampleEntity.load('id')
let derivedEntity = entity.derived_entity

As of version 0.6.0, this is done by using the loadRelated function of graph-node, the derived entities can be accessed the same way as in the handlers.

test('Derived fields example test', () => {
let mainAccount = GraphAccount.load('12')!
assert.assertNull(mainAccount.get('nameSignalTransactions'))
assert.assertNull(mainAccount.get('operatorOf'))
let operatedAccount = GraphAccount.load('1')!
operatedAccount.operators = [mainAccount.id]
operatedAccount.save()
mockNameSignalTransaction('1234', mainAccount.id)
mockNameSignalTransaction('2', mainAccount.id)
mainAccount = GraphAccount.load('12')!
assert.assertNull(mainAccount.get('nameSignalTransactions'))
assert.assertNull(mainAccount.get('operatorOf'))
const nameSignalTransactions = mainAccount.nameSignalTransactions.load()
const operatorsOfMainAccount = mainAccount.operatorOf.load()
assert.i32Equals(2, nameSignalTransactions.length)
assert.i32Equals(1, operatorsOfMainAccount.length)
assert.stringEquals('1', operatorsOfMainAccount[0].id)
mockNameSignalTransaction('2345', mainAccount.id)
let nst = NameSignalTransaction.load('1234')!
nst.signer = '11'
nst.save()
store.remove('NameSignalTransaction', '2')
mainAccount = GraphAccount.load('12')!
assert.i32Equals(1, mainAccount.nameSignalTransactions.load().length)
})

Testing loadInBlock

Enlace a esta sección

As of version 0.6.0, users can test loadInBlock by using the mockInBlockStore, it allows mocking entities in the block cache.

import { afterAll, beforeAll, describe, mockInBlockStore, test } from 'matchstick-as'
import { Gravatar } from '../../generated/schema'
describe('loadInBlock', () => {
beforeAll(() => {
mockInBlockStore('Gravatar', 'gravatarId0', gravatar)
})
afterAll(() => {
clearInBlockStore()
})
test('Can use entity.loadInBlock() to retrieve entity from cache store in the current block', () => {
let retrievedGravatar = Gravatar.loadInBlock('gravatarId0')
assert.stringEquals('gravatarId0', retrievedGravatar!.get('id')!.toString())
})
test("Returns null when calling entity.loadInBlock() if an entity doesn't exist in the current block", () => {
let retrievedGravatar = Gravatar.loadInBlock('IDoNotExist')
assert.assertNull(retrievedGravatar)
})
})

Probar fuentes de datos dinámicas

Enlace a esta sección

La prueba de las fuentes de datos dinámicas se puede realizar simulando el valor de retorno de las funciones context(), address() y network() del dataSource namespace. Estas funciones actualmente devuelven lo siguiente: context() - devuelve una entidad vacía (DataSourceContext), address() - devuelve 0x00000000000000000000000000000000000000000, network() - devuelve mainnet. Las funciones create(...) y createWithContext(...) son mocked para no hacer nada, por lo que no es necesario llamarlas en las pruebas. Los cambios en los valores devueltos se pueden realizar a través de las funciones del espacio de nombres dataSourceMock en matchstick-as (versión 0.3.0+).

Ejemplo a continuación:

Primero, tenemos el siguiente handler de eventos (que se ha reutilizado intencionalmente para mostrar el mocking de datasource):

export function handleApproveTokenDestinations(event: ApproveTokenDestinations): void {
let tokenLockWallet = TokenLockWallet.load(dataSource.address().toHexString())!
if (dataSource.network() == 'rinkeby') {
tokenLockWallet.tokenDestinationsApproved = true
}
let context = dataSource.context()
if (context.get('contextVal')!.toI32() > 0) {
tokenLockWallet.setBigInt('tokensReleased', BigInt.fromI32(context.get('contextVal')!.toI32()))
}
tokenLockWallet.save()
}

Y luego tenemos la prueba usando uno de los métodos en el namespace dataSourceMock para establecer un nuevo valor de retorno para todas las funciones de dataSource:

import { assert, test, newMockEvent, dataSourceMock } from 'matchstick-as/assembly/index'
import { BigInt, DataSourceContext, Value } from '@graphprotocol/graph-ts'
import { handleApproveTokenDestinations } from '../../src/token-lock-wallet'
import { ApproveTokenDestinations } from '../../generated/templates/GraphTokenLockWallet/GraphTokenLockWallet'
import { TokenLockWallet } from '../../generated/schema'
test('Data source simple mocking example', () => {
let addressString = '0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'
let address = Address.fromString(addressString)
let wallet = new TokenLockWallet(address.toHexString())
wallet.save()
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
dataSourceMock.setReturnValues(addressString, 'rinkeby', context)
let event = changetype<ApproveTokenDestinations>(newMockEvent())
assert.assertTrue(!wallet.tokenDestinationsApproved)
handleApproveTokenDestinations(event)
wallet = TokenLockWallet.load(address.toHexString())!
assert.assertTrue(wallet.tokenDestinationsApproved)
assert.bigIntEquals(wallet.tokensReleased, BigInt.fromI32(325))
dataSourceMock.resetValues()
})

Tenga en cuenta que dataSourceMock.resetValues() se llama al final. Esto se debe a que los valores se recuerdan cuando se modifican y deben restablecerse si desea volver a los valores predeterminados.

Testing dynamic data source creation

Enlace a esta sección

As of version 0.6.0, it is possible to test if a new data source has been created from a template. This feature supports both ethereum/contract and file/ipfs templates. There are four functions for this:

  • assert.dataSourceCount(templateName, expectedCount) can be used to assert the expected count of data sources from the specified template
  • assert.dataSourceExists(templateName, address/ipfsHash) asserts that a data source with the specified identifier (could be a contract address or IPFS file hash) from a specified template was created
  • logDataSources(templateName) prints all data sources from the specified template to the console for debugging purposes
  • readFile(path) reads a JSON file that represents an IPFS file and returns the content as Bytes

Testing ethereum/contract templates

Enlace a esta sección
test('ethereum/contract dataSource creation example', () => {
// Assert there are no dataSources created from GraphTokenLockWallet template
assert.dataSourceCount('GraphTokenLockWallet', 0)
// Create a new GraphTokenLockWallet datasource with address 0xA16081F360e3847006dB660bae1c6d1b2e17eC2A
GraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))
// Assert the dataSource has been created
assert.dataSourceCount('GraphTokenLockWallet', 1)
// Add a second dataSource with context
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)
// Assert there are now 2 dataSources
assert.dataSourceCount('GraphTokenLockWallet', 2)
// Assert that a dataSource with address "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B" was created
// Keep in mind that `Address` type is transformed to lower case when decoded, so you have to pass the address as all lower case when asserting if it exists
assert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())
logDataSources('GraphTokenLockWallet')
})
Example logDataSource output
Enlace a esta sección
🛠 {
"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
"context": null
},
"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b",
"context": {
"contextVal": {
"type": "Int",
"data": 325
}
}
}
}

Testing file/ipfs templates

Enlace a esta sección

Similarly to contract dynamic data sources, users can test test file datas sources and their handlers

Example subgraph.yaml
Enlace a esta sección
...
templates:
- kind: file/ipfs
name: GraphTokenLockMetadata
network: mainnet
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/token-lock-wallet.ts
handler: handleMetadata
entities:
- TokenLockMetadata
abis:
- name: GraphTokenLockWallet
file: ./abis/GraphTokenLockWallet.json
Example schema.graphql
Enlace a esta sección
"""
Token Lock Wallets which hold locked GRT
"""
type TokenLockMetadata @entity {
"The address of the token lock wallet"
id: ID!
"Start time of the release schedule"
startTime: BigInt!
"End time of the release schedule"
endTime: BigInt!
"Number of periods between start time and end time"
periods: BigInt!
"Time when the releases start"
releaseStartTime: BigInt!
}
Example metadata.json
Enlace a esta sección
{
"startTime": 1,
"endTime": 1,
"periods": 1,
"releaseStartTime": 1
}
Example handler
Enlace a esta sección
export function handleMetadata(content: Bytes): void {
// dataSource.stringParams() returns the File DataSource CID
// stringParam() will be mocked in the handler test
// for more info https://thegraph.com/docs/en/developing/creating-a-subgraph/#create-a-new-handler-to-process-files
let tokenMetadata = new TokenLockMetadata(dataSource.stringParam())
const value = json.fromBytes(content).toObject()
if (value) {
const startTime = value.get('startTime')
const endTime = value.get('endTime')
const periods = value.get('periods')
const releaseStartTime = value.get('releaseStartTime')
if (startTime && endTime && periods && releaseStartTime) {
tokenMetadata.startTime = startTime.toBigInt()
tokenMetadata.endTime = endTime.toBigInt()
tokenMetadata.periods = periods.toBigInt()
tokenMetadata.releaseStartTime = releaseStartTime.toBigInt()
}
tokenMetadata.save()
}
}
import { assert, test, dataSourceMock, readFile } from 'matchstick-as'
import { Address, BigInt, Bytes, DataSourceContext, ipfs, json, store, Value } from '@graphprotocol/graph-ts'
import { handleMetadata } from '../../src/token-lock-wallet'
import { TokenLockMetadata } from '../../generated/schema'
import { GraphTokenLockMetadata } from '../../generated/templates'
test('file/ipfs dataSource creation example', () => {
// Generate the dataSource CID from the ipfsHash + ipfs path file
// For example QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.json
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
const CID = `${ipfshash}/example.json`
// Create a new dataSource using the generated CID
GraphTokenLockMetadata.create(CID)
// Assert the dataSource has been created
assert.dataSourceCount('GraphTokenLockMetadata', 1)
assert.dataSourceExists('GraphTokenLockMetadata', CID)
logDataSources('GraphTokenLockMetadata')
// Now we have to mock the dataSource metadata and specifically dataSource.stringParam()
// dataSource.stringParams actually uses the value of dataSource.address(), so we will mock the address using dataSourceMock from matchstick-as
// First we will reset the values and then use dataSourceMock.setAddress() to set the CID
dataSourceMock.resetValues()
dataSourceMock.setAddress(CID)
// Now we need to generate the Bytes to pass to the dataSource handler
// For this case we introduced a new function readFile, that reads a local json and returns the content as Bytes
const content = readFile(`path/to/metadata.json`)
handleMetadata(content)
// Now we will test if a TokenLockMetadata was created
const metadata = TokenLockMetadata.load(CID)
assert.bigIntEquals(metadata!.endTime, BigInt.fromI32(1))
assert.bigIntEquals(metadata!.periods, BigInt.fromI32(1))
assert.bigIntEquals(metadata!.releaseStartTime, BigInt.fromI32(1))
assert.bigIntEquals(metadata!.startTime, BigInt.fromI32(1))
})

Cobertura de prueba

Enlace a esta sección

Usando Matchstick, los desarrolladores de subgrafos pueden ejecutar un script que calculará la cobertura de las pruebas unitarias escritas.

The test coverage tool takes the compiled test wasm binaries and converts them to wat files, which can then be easily inspected to see whether or not the handlers defined in subgraph.yaml have been called. Since code coverage (and testing as whole) is in very early stages in AssemblyScript and WebAssembly, Matchstick cannot check for branch coverage. Instead we rely on the assertion that if a given handler has been called, the event/function for it have been properly mocked.

Prerrequisitos

Enlace a esta sección

Para ejecutar la funcionalidad de cobertura de prueba proporcionada en Matchstick, hay algunas cosas que debe preparar de antemano:

Exporta tus handlers

Enlace a esta sección

Para que Matchstick verifique qué handlers se están ejecutando, esos handlers deben exportarse desde el archivo de prueba. Entonces, por ejemplo, en nuestro archivo gravity.test.ts tenemos el siguiente handler que se está importando:

import { handleNewGravatar } from '../../src/gravity'

Para que esa función sea visible (para que se incluya en el archivo wat por nombre) también debemos exportarla, como este:

export { handleNewGravatar }

Una vez que esté todo configurado, para ejecutar la herramienta de cobertura de prueba, simplemente ejecuta:

graph test -- -c

También puedes agregar un comando coverage personalizado a tu archivo package.json, así:

"scripts": {
/.../
"coverage": "graph test -- -c"
},

Eso ejecutará la herramienta de cobertura y deberías ver algo como esto en la terminal:

$ graph test -c
Skipping download/install step because binary already exists at /Users/petko/work/demo-subgraph/node_modules/binary-install-raw/bin/0.4.0
___ ___ _ _ _ _ _
| \/ | | | | | | | (_) | |
| . . | __ _| |_ ___| |__ ___| |_ _ ___| | __
| |\/| |/ _` | __/ __| '_ \/ __| __| |/ __| |/ /
| | | | (_| | || (__| | | \__ \ |_| | (__| <
\_| |_/\__,_|\__\___|_| |_|___/\__|_|\___|_|\_\
Compiling...
Running in coverage report mode.
Reading generated test modules... 🔎️
Generating coverage report 📝
Handlers for source 'Gravity':
Handler 'handleNewGravatar' is tested.
Handler 'handleUpdatedGravatar' is not tested.
Handler 'handleCreateGravatar' is tested.
Test coverage: 66.7% (2/3 handlers).
Handlers for source 'GraphTokenLockWallet':
Handler 'handleTokensReleased' is not tested.
Handler 'handleTokensWithdrawn' is not tested.
Handler 'handleTokensRevoked' is not tested.
Handler 'handleManagerUpdated' is not tested.
Handler 'handleApproveTokenDestinations' is not tested.
Handler 'handleRevokeTokenDestinations' is not tested.
Test coverage: 0.0% (0/6 handlers).
Global test coverage: 22.2% (2/9 handlers).

Duración del tiempo de ejecución de la prueba en la salida del log

Enlace a esta sección

La salida del log incluye la duración de la ejecución de la prueba. Aquí hay un ejemplo:

[Thu, 31 Mar 2022 13:54:54 +0300] Programa ejecutado en: 42.270ms.

Errores comunes del compilador

Enlace a esta sección

Critical: Could not create WasmInstance from valid module with context: unknown import: wasi_snapshot_preview1::fd_write has not been defined

Esto significa que has utilizado console.log en tu código, que no es compatible con AssemblyScript. Considera usar la API de registro

ERROR TS2554: Expected ? arguments, but got ?.

return new ethereum.Block(defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultAddress, defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt);

in ~lib/matchstick-as/assembly/defaults.ts(18,12)

ERROR TS2554: Expected ? arguments, but got ?.

return new ethereum.Transaction(defaultAddressBytes, defaultBigInt, defaultAddress, defaultAddress, defaultBigInt, defaultBigInt, defaultBigInt, defaultAddressBytes, defaultBigInt);

in ~lib/matchstick-as/assembly/defaults.ts(24,12)

La falta de coincidencia en los argumentos se debe a la falta de coincidencia en graph-ts y matchstick-as. La mejor manera de solucionar problemas como este es actualizar todo a la última versión publicada.

Si tiene preguntas, comentarios, solicitudes de funciones o simplemente deseas comunicarte, el mejor lugar sería The Graph Discord, donde tenemos un canal dedicado para Matchstick, llamado 🔥| unit-testing.

Editar página

Anterior
Problemas comunes de AssemblyScript
Siguiente
Preguntas Frecuentes de los Desarrolladores
Editar página