Marco de Unit Testing
Reading time: 26 min
¡Matchstick es un marco de unit testing, desarrollado por , que permite a los developers de subgrafos probar su lógica de mapeo en un entorno sandbox y deployar sus subgrafos con confianza!
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
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 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 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
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/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
Puedes probar y jugar con los ejemplos de esta guía clonando el
También puedes ver la serie de videos en
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 Gravatarassert.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/0x0assert.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 Gravatarassert.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/0x0assert.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 Gravatarassert.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/0x0assert.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',)
Veamos cómo se vería un unit test simple usando los ejemplos de Gravatar en el .
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.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.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 eventslet newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')// Call mapping functions passing the events we just createdhandleNewGravatars([newGravatarEvent, anotherGravatarEvent])// Assert the state of the storeassert.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 slateclearStore()})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 . 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óncreateNewGravatarEvent()
; - 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:
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()
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)
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);});}
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()
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 itexport { 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 callbackexport function processGravatar(value: JSONValue, userData: Value): void {// See the JSONValue documentation for details on dealing// with JSON valueslet obj = value.toObject()let id = obj.get('id')if (!id) {return}// Callbacks can also created entitieslet gravatar = new Gravatar(id.toString())gravatar.displayName = userData.toString() + id.toString()gravatar.save()}// function that calls ipfs.catexport 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()}
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.
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:
// Readlet logType = newGravatarEvent.logType// Writelet UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));
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')
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)
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 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)})
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)})})
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.
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 templateassert.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 createdlogDataSources(templateName)
prints all data sources from the specified template to the console for debugging purposesreadFile(path)
reads a JSON file that represents an IPFS file and returns the content as Bytes
test('ethereum/contract dataSource creation example', () => {// Assert there are no dataSources created from GraphTokenLockWallet templateassert.dataSourceCount('GraphTokenLockWallet', 0)// Create a new GraphTokenLockWallet datasource with address 0xA16081F360e3847006dB660bae1c6d1b2e17eC2AGraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))// Assert the dataSource has been createdassert.dataSourceCount('GraphTokenLockWallet', 1)// Add a second dataSource with contextlet context = new DataSourceContext()context.set('contextVal', Value.fromI32(325))GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)// Assert there are now 2 dataSourcesassert.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 existsassert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())logDataSources('GraphTokenLockWallet')})
🛠 {"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {"kind": "ethereum/contract","name": "GraphTokenLockWallet","address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a","context": null},"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {"kind": "ethereum/contract","name": "GraphTokenLockWallet","address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b","context": {"contextVal": {"type": "Int","data": 325}}}}
Similarly to contract dynamic data sources, users can test test file datas sources and their handlers
...templates:- kind: file/ipfsname: GraphTokenLockMetadatanetwork: mainnetmapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/token-lock-wallet.tshandler: handleMetadataentities:- TokenLockMetadataabis:- name: GraphTokenLockWalletfile: ./abis/GraphTokenLockWallet.json
"""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!}
{"startTime": 1,"endTime": 1,"periods": 1,"releaseStartTime": 1}
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-fileslet 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.jsonconst ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'const CID = `${ipfshash}/example.json`// Create a new dataSource using the generated CIDGraphTokenLockMetadata.create(CID)// Assert the dataSource has been createdassert.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 CIDdataSourceMock.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 Bytesconst content = readFile(`path/to/metadata.json`)handleMetadata(content)// Now we will test if a TokenLockMetadata was createdconst 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))})
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.
Para ejecutar la funcionalidad de cobertura de prueba proporcionada en Matchstick, hay algunas cosas que debe preparar de antemano:
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 -cSkipping 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).
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.
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
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.