Desarrollando > Marco de Unit Testing

Marco de Unit Testing

Reading time: 21 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.30.0",
"@graphprotocol/graph-ts": "^0.27.0",
"matchstick-as": "^0.5.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.

❗ 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"

Estructura de las pruebas (>=0.5.0)

Enlace a esta sección

IMPORTANTE: Requiere matchstick-as >=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)

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

Impresión del almacenamiento entero (para fines de depuración)

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

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 de campos derivados es una función que (como muestra el siguiente ejemplo) permite al usuario establecer un campo en una determinada entidad y hacer que otra entidad se actualice automáticamente si deriva uno de sus campos de la primera entidad. Lo importante a tener en cuenta es que la primera entidad debe recargarse ya que la actualización automática ocurre en el store en rust de la cual el código AS es independiente.

test('Derived fields example test', () => {
let mainAccount = new GraphAccount('12')
mainAccount.save()
let operatedAccount = new GraphAccount('1')
operatedAccount.operators = ['12']
operatedAccount.save()
let nst = new NameSignalTransaction('1234')
nst.signer = '12'
nst.save()
assert.assertNull(mainAccount.get('nameSignalTransactions'))
assert.assertNull(mainAccount.get('operatorOf'))
mainAccount = GraphAccount.load('12')!
assert.i32Equals(1, mainAccount.nameSignalTransactions.length)
assert.stringEquals('1', mainAccount.operatorOf[0])
})

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.

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