Developing > Estrutura de Testes de Unidades

Estrutura de Testes de Unidades

Reading time: 26 min

O Matchstick é uma estrutura de testes de unidades, desenvolvida pela LimeChain, que permite que programadores de subgraph testem a sua lógica de mapeamento num ambiente de sandbox e lancem os seus subgraphs com confiança!

Como Começar

Link para esta seção

Dependências de instalação

Link para esta seção

Para usar os métodos de test helper e executar os testes, instale as seguintes dependências:

yarn add --dev matchstick-as

❗ O graph-node depende do PostgreSQL, então se ainda não o tens, precisa instalá-lo. Recomendamos fortemente usar os comandos abaixo, pois adicioná-lo de qualquer outra maneira pode causar erros inesperados!

Comando de instalação do Postgres:

brew install postgresql

Crie um symlink ao último libpq.5.lib Você pode precisar criar este diretório primeiro /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 instalação do Postgres (depende da sua distro):

sudo apt install postgresql

WSL (Windows Subsystem for Linux)

Link para esta seção

Pode usar o Matchstick no WSL tanto com a abordagem do Docker quanto com a abordagem binária. Como o WSL pode ser um pouco complicado, aqui estão algumas dicas caso encontre problemas

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

ou

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

Tenha ciência que está em uma versão mais recente do Node.js. O graph-cli não apoia mais a v10.19.0, que ainda é a versão padrão para novas imagens de Ubuntu no WSL. Por exemplo, se o Matchstick é confirmado como funcional no WSL com a v18.1.0, pode trocar para essa versão através do nvm ou ao atualizar o seu Node.js global. Não se esqueça de apagar o node_modules e executar o npm install novamente após atualizar o seu nodejs! Depois, garanta que tem o libpq instalado. Pode fazer isto ao executar

sudo apt-get install libpq-dev

E finalmente, não use o graph test (que usa a sua instalação global do graph-cli, e por alguma razão, parece não funcionar no WSL no momento). Em vez disto, use o yarn test ou o npm run test (que usará a instância local do graph-cli; esta funciona muito bem). Para isto, obviamente precisaria de um script test no seu arquivo package.json, que pode ser algo simples 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 o Matchstick no seu projeto de subgraph, basta abrir um terminal, navegar à pasta raiz do seu projeto e executar graph test [options] <datasource> — este baixa o binário mais recente do Matchstick, e executa o teste especificado, ou todos os testes especificados em uma pasta de teste (ou todos os testes existentes se não for especificado nenhum flag de fontes de dados).

Opções de CLI

Link para esta seção

Isto executará todos os testes na pasta-teste:

graph test

Isto executará um teste chamado gravity.test.ts e/ou todos os testes dentro de uma pasta chamada gravity:

graph test gravity

Isto só executará esse arquivo de teste específico:

graph test path/to/file.test.ts

Opções:

-c, --coverage Executa os testes em modo de cobertura
-d, --docker Executa os testes em um docker container (Nota: Favor executar da pasta raiz do subgraph)
-f --force Binário: Baixa o binário novamente. Docker: Baixa o Dockerfile novamente e reconstroi a imagem do docker.
-h, --help Mostra informações de uso
-l, --logs Mostra no console informações sobre o sistema operacional, modelo de CPU, e URL de download (para propósitos de debugging)
-r, --recompile Força os testes a serem recompilados
-v, --version <tag> Escolhe a versão do binário rust que você deseja baixar/usar

Desde o graph-cli 0.25.2, o comando graph test apoia a execução do matchstick em um container docker com a flag -d. A implementação do docker utiliza o bind mount para que não precise reconstruir a imagem do docker toda vez que o comando graph test -d é executado. Alternativamente, siga as instruções do repositório do matchstick para executar o docker manualmente.

graph test -d força o docker run a ser executado com o flag -t. Isto deve ser removido para rodar dentro de ambientes não interativos (como o GitHub CI).

❗ Caso já tenha executado o graph test anteriormente, pode encontrar o seguinte erro durante a construção do docker:

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

Neste caso, crie um .dockerignore na pasta raiz e adicione node_modules/binary-install-raw/bin

Configuração

Link para esta seção

O Matchstick pode ser configurado para usar um caminho personalizado de tests, libs e manifest através do arquivo de configuração matchstick.yaml:

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

Subgraph de demonstração

Link para esta seção

Podes experimentar e experimentar com os exemplos deste guia ao clonar a repo do Subgraph de Demonstração

Tutoriais de vídeo

Link para esta seção

Também pode conferir a série em vídeo sobre "Como usar o Matchstick para escrever testes de unidade para os seus subgraphs"

Estrutura de testes

Link para esta seção

IMPORTANTE: A estrutura de teste descrita abaixo depende da versão >=0.5.0 do matchstick-as

describe(name: String , () => {}) — Define um grupo de teste.

Notas:

  • Describes (descrições) não são obrigatórias. Ainda pode usar o test() da maneira antiga, fora dos blocos describe()

Exemplo:

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

Exemplo aninhado 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 um caso de teste. Pode usar o test() em blocos describe() ou de maneira independente.

Exemplo:

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

ou

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

Executa um bloco de código antes de quaisquer dos testes no arquivo. Se o beforeAll for declarado dentro de um bloco describe, ele é executado no começo daquele bloco describe.

Exemplos:

O código dentro do beforeAll será executado uma vez antes de todos os testes no arquivo.

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", () => {
...
})
})

O código antes do beforeAll será executado uma vez antes de todos os testes no primeiro bloco 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", () => {
...
})
})

Executa um bloco de código depois de todos os testes no arquivo. Se o afterAll for declarado dentro de um bloco describe, ele será executado no final daquele bloco describe.

Exemplo:

O código dentro do afterAll será executado uma vez depois de todos os testes no arquivo.

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", () => {
...
})
})

O código dentro do afterAll será executado uma vez depois de todos os testes no bloco 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", () => {
...
})
})

Executa um bloco de código antes de cada teste no arquivo. Se o beforeEach for declarado dentro de um bloco describe, ele será executado antes de cada teste naquele bloco describe.

Exemplos: O código dentro do beforeEach será executado antes de cada teste.

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", () => {
...
})
})
...

O código antes do beforeEach será executado antes de cada teste no 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')
})
})

Executa um bloco de código depois de cada teste no arquivo. Se o afterEach for declarado dentro de um bloco describe, será executado após cada teste naquele bloco describe.

Exemplos:

O código dentro do afterEach será executado após cada teste.

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

O código dentro do afterEach será executado após cada teste naquele 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)

A partir da versão 0.6.0, asserts também apoiam mensagens de erro personalizadas

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

Como Escrever um Teste de Unidade

Link para esta seção

Vamos ver como um simples teste de unidade pareceria, com os exemplos do Gravatar no Subgraph de Demonstração.

Suponhamos que temos a seguinte função de handler (com duas funções de helper para facilitar):

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
}

Primeiro, devemos criar um arquivo de teste no nosso projeto. Este é um exemplo de como ele pode ficar:

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', () => {
// Cria uma entidade de teste e salve-a no armazenamento como um estado inicial (opcional)
let gravatar = new Gravatar('gravatarId0')
gravatar.save()
// Cria eventos falsos
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
// Chama funções de mapeamento ao passar os eventos que acabamos de criar
handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
// Afirma o estado do armazenamento
assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')
// Limpa o armazenamento para começar o próximo teste do zero
clearStore()
})
test('Next test', () => {
//...
})

Quanta coisa! Primeiro, note que estamos a impoortar coisas do matchstick-as, nossa biblioteca de helper do AssemblyScript (distribuída como um módulo npm). Pode encontrar o repositório aqui. matchstick-as nos dá alguns métodos de teste úteis e define a função test(), que usaremos para construir os nossos blocos de teste. O resto é bem simples — veja o que acontece:

  • Configuramos nosso estado inicial e adicionamos uma entidade de Gravatar personalizada;
  • Definimos dois eventos NewGravatar com os seus dados, usando a função createNewGravatarEvent();
  • Chamamos métodos de handlers para estes eventos — handleNewGravatars() e passamos a lista dos nossos eventos personalizados;
  • Garantimos o estado da loja. Como isto funciona? — Passamos uma combinação do tipo e da id da Entidade. Depois conferimos um campo específico naquela Entidade e garantimos que ela tem o valor que esperamos que tenha. Estamos a fazer isto tanto para a Entidade Gravatar inicial adicionada ao armazenamento, quanto para as duas entidades Gravatar adicionadas ao chamar a função de handler;
  • E por último — limpamos o armazenamento com clearStore(), para que o nosso próximo teste comece com um objeto de armazenamento novo em folha. Podemos definir quantos blocos de teste quisermos.

Prontinho — criamos o nosso primeiro teste! 👏

Para executar os nossos testes, basta apenas executar o seguinte na pasta raiz do seu subgraph:

graph test Gravity

E se tudo der certo, deve receber a seguinte resposta:

Matchstick diz "All tests passed!" (Todos os testes passados!)

Cenários de teste comuns

Link para esta seção

Como hidratar o armazenamento com um certo estado

Link para esta seção

Os utilizadores podem hidratar o armazenamento com um conjunto conhecido de entidades. Aqui está um exemplo para inicializar o armazenamento com uma entidade Gravatar:

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

Como chamar uma função de mapeamento com um evento

Link para esta seção

Um utilizador pode criar um evento personalizado e passá-lo a uma função de mapeamento ligada ao armazenamento:

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)

Como chamar todos os mapeamentos com fixações de eventos

Link para esta seção

Os utilizadores podem chamar os mapeamentos com fixações de teste.

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

Como simular chamadas de contratos

Link para esta seção

Os utilizadores podem simular chamadas de contratos:

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 demonstrado, para simular uma chamada de contrato e conseguir um valor de retorno de linha-dura, o utilizador deve fornecer um endereço de contrato, nome de função, assinatura de função, arranjo de argumentos — e claro, o valor de retorno.

Os utilizadores também podem simular regressos de funções:

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

Como simular arquivos IPFS (do matchstick 0.4.1)

Link para esta seção

Os utilizadores podem simular arquivos IPFS com a função mockIpfsFile(hash, filePath). A função aceita dois argumentos: o primeiro é o hash/caminho do arquivo IPFS, e o segundo é o caminho a um arquivo local.

NOTA: Ao testar o ipfs.map/ipfs.mapJSON, a função de callback deve ser exportada do arquivo de teste para que o matchstick o detete, como a função processGravatar() no exemplo de teste abaixo:

Arquivo .test.ts:

import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'
import { ipfs } from '@graphprotocol/graph-ts'
import { gravatarFromIpfs } from './utils'
// Exporta o callback do ipfs.map() para que o matchstick o detete
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')
})

Arquivo utils.ts:

import { Address, ethereum, JSONValue, Value, ipfs, json, Bytes } from "@graphprotocol/graph-ts"
import { Gravatar } from "../../generated/schema"
...
// callback do ipfs.map
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 também podem criar entidades
let gravatar = new Gravatar(id.toString())
gravatar.displayName = userData.toString() + id.toString()
gravatar.save()
}
// função que chama o 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()
}

Como afirmar o estado do armazenamento

Link para esta seção

Os utilizadores podem afirmar o estado final (ou parcial) do armazenamento através de entidades de afirmação. Para isto, o utilizador precisa fornecer um tipo de Entidade, a ID específica de uma Entidade, o nome de um campo naquela Entidade, e o valor esperado do campo. Aqui vai um exemplo 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')

A função assert.fieldEquals() conferirá a igualdade do campo dado contra o valor dado esperado. O teste acabará em erro, com mensagem correspondente, caso os valores NÃO sejam iguais. Caso contrário, o teste terá êxito.

Como interagir com metadados de Eventos

Link para esta seção

Os utilizadores podem usar metadados-padrão de transações, que podem ser retornados como um ethereum.Event com a função newMockEvent(). O seguinte exemplo mostra como pode ler/escrever a estes campos no objeto de Evento:

// Leitura
let logType = newGravatarEvent.logType
// Escrita
let UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'
newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)

Como afirmar a igualdade das variáveis

Link para esta seção
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

Como afirmar que uma Entidade não está no armazenamento

Link para esta seção

Os utilizadores podem afirmar que uma entidade não existe no armazenamento. A função toma um tipo e uma id de entidade. Caso a entidade esteja, de facto, na loja, o teste acabará em erro, com uma mensagem de erro relevante. Veja um exemplo rápido de como usar esta funcionalidade:

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

Impressão do armazenamento completo, ou de entidades individuais dele (para debugging)

Link para esta seção

Pode imprimir o armazenamento inteiro na consola com esta função de helper:

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

Desde a versão 0.6.0, o logStore não imprime mais campos derivados; em vez disto, utilizadores podem usar a nova função logEntity. O logEntity pode ser usado para imprimir qualquer entidade, não só as que têm campos derivados. O logEntity pega o tipo e a ID da entidade e um flag showRelated para indicar se utilizadores querem imprimir as entidades derivadas relacionadas.

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

Falhas esperadas

Link para esta seção

Os utilizadores podem encontrar falhas esperadas, com o flag shouldFail nas funções test():

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

Caso o teste seja marcado com shouldFail = true mas NÃO falhe, isto será mostrado como um erro nos logs e o bloco de teste não terá êxito. E se for marcado com shouldFail = false (o estado normal), o executor de teste travará.

Ter logs personalizados nos testes de unidade é a mesma coisa que logar nos mapeamentos. A diferença é que o objeto do log deve ser importado do matchstick-as, em vez do graph-ts. Aqui vai um exemplo simples com todos os tipos de log não-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!", []);
});

Os utilizadores também podem simular uma falha crítica, como no seguinte:

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

Logar erros críticos interromperá a execução dos testes e causará um desastre. Afinal, queremos ter certeza que o seu código não tenha logs críticos no lançamento; perceberia imediatamente se isto acontecer.

Como testar campos derivados

Link para esta seção

Testar campos derivados permite aos utilizadores configurar um campo numa entidade e atualizar outra automaticamente, caso ela derive um dos seus campos da primeira entidade.

Antes da versão 0.6.0, era possível resgatar as entidades derivadas ao acessá-las como propriedades ou campos de entidade, como no seguinte exemplo:

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

Desde a versão 0.6.0, isto é feito com a função loadRelated do graph-node. As entidades derivadas podem ser acessadas como são nos 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)
})

Teste de loadInBlock

Link para esta seção

Desde a versão 0.6.0, é possível testar o loadInBlock com o mockInBlockStore, que permite o mocking de entidades no cache de blocos.

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

Como testar fontes de dados dinâmicas

Link para esta seção

Testar fontes de dados dinâmicas pode ser feito ao falsificar o valor de retorno das funções context(), address() e network() do namespace do dataSource. Estas funções atualmente retornam o seguinte: context() — retorna uma entidade vazia (DataSourceContext), address() — retorna 0x0000000000000000000000000000000000000000, network() — retorna mainnet. As funções create(...) e createWithContext(...) são falsificadas para não terem uso, para que não precisem ser chamadas nos teste. Dá para mudar os valores de retorno podem através das funções do namespace dataSourceMock no matchstick-as (versão 0.3.0+).

Exemplo abaixo:

Primeiro temos o seguinte handler de eventos (que foi apropriado intencionalmente para demonstrar a falsificação de fontes de dados):

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

E então, temos o teste que usa um dos métodos do namespace dataSourceMock para determinar um novo valor de retorno para todas as funções do 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()
})

Note que o dataSourceMock.resetValues() é chamado no final. Isto é porque os valores são lembrados quando mudados, e devem ser reconfigurados caso queira voltar aos valores padrão.

Teste de criação de fontes de dados dinâmicas

Link para esta seção

Desde a versão 0.6.0, é possível testar se uma nova fonte de dados foi criada de um modelo. Este recurso apoia modelos do ethereum/contract e do file/ipfs. Há quatro funçôes para isto:

  • assert.dataSourceCount(templateName, expectedCount) pode ser usado para impor a contagem esperada de fontes de dados do modelo especificado
  • assert.dataSourceExists(templateName, address/ipfsHash) impõe que foi criada uma fonte de dados com o identificador especificado (pode ser um endereço de contrato ou um hash de arquivo ipfs) de um modelo especificado
  • logDataSources(templateName) imprime todas as fontes de dados do modelo especificado ao console para propósitos de debugging
  • readFile(path) lê um arquivo JSON que representa um arquivo IPFS e retorna o conteúdo como Bytes

Teste de modelos ethereum/contract

Link para esta seção
test('ethereum/contract dataSource creation example', () => {
// Garanta que não há dataSources criadas do modelo do GraphTokenLockWallet
assert.dataSourceCount('GraphTokenLockWallet', 0)
// Crie uma nova fonte de dados GraphTokenLockWallet com o endereço
0xa16081f360e3847006db660bae1c6d1b2e17ec2a
GraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))
// Garanta que a dataSource foi criada
assert.dataSourceCount('GraphTokenLockWallet', 1)
// Adicione uma segunda dataSource com contexto
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)
// Garanta que agora há 2 dataSources
assert.dataSourceCount('GraphTokenLockWallet', 2)
// Garanta que uma dataSource com o endereço "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B" foi criada
// Lembre-se que o tipo `Address` foi transformado em letra minúscula após decodificado, então é necessário passar o endereço em letras minúsculas ao garantir que ele existe
assert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())
logDataSources('GraphTokenLockWallet')
})
Exemplo de resultado de logDataSource
Link para esta seção
🛠 {
"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
"context": null
},
"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b",
"context": {
"contextVal": {
"type": "Int",
"data": 325
}
}
}
}

Teste de modelos file/ipfs

Link para esta seção

Assim como as fontes dinâmicas de dados de contrato, os utilizadores podem testar fontes de dados de arquivos e os seus handlers

Exemplo de subgraph.yaml
Link para esta seção
...
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
Exemplo de schema.graphql
Link para esta seção
"""
Token Lock Wallets que têm GRT trancado
"""
type TokenLockMetadata @entity {
"Endereço da token lock wallet"
id: ID!
"Começo da agenda de lançamento"
startTime: BigInt!
"Final da agenda de lançamento"
endTime: BigInt!
"Número de períodos entre o início e o fim"
periods: BigInt!
"Hora quando os lançamentos começam"
releaseStartTime: BigInt!
}
Exemplo de metadata.json
Link para esta seção
{
"startTime": 1,
"endTime": 1,
"periods": 1,
"releaseStartTime": 1
}
Exemplo de handler
Link para esta seção
export function handleMetadata(content: Bytes): void {
// dataSource.stringParams() retorna a CID File DataSource
// haverá um mock de stringParam() no teste de handler
// para mais informações, leia 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()
}
}
Exxemplo de teste
Link para esta seção
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', () => {
// Gere a CID dataSource do arquivo de caminho ipfsHash + ipfs
// Por exemplo QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.json
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
const CID = `${ipfshash}/example.json`
// Crie uma nova dataSource com a CID gerada
GraphTokenLockMetadata.create(CID)
// Garanta que a dataSource foi criada
assert.dataSourceCount('GraphTokenLockMetadata', 1)
assert.dataSourceExists('GraphTokenLockMetadata', CID)
logDataSources('GraphTokenLockMetadata')
// Agora tempos que fazer um mock dos metadados da dataSource e especificamente utilizar dataSource.stringParam()
// dataSource.stringParams usa o valor do dataSource.address(), então faremos um mock do endereço com odataSourceMock do matchstick-as
// Primeiro reiniciaremos os valores e depois usaremos o dataSourceMock.setAddress() para configurar a CID
dataSourceMock.resetValues()
dataSourceMock.setAddress(CID)
// Agora precisamos gerar os Bytes para repassar ao handler dataSource
// Para este caso, apresentamos uma nova função readFile que lê um json local e retorna o conteúdo como Bytes
const content = readFile(`path/to/metadata.json`)
handleMetadata(content)
// Agora testaremos se um TokenLockMetadata foi criado
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 Testes

Link para esta seção

Com o Matchstick, os programadores de subgraph podem executar um script que calcula a cobertura de teste das unidades de teste escritas.

A ferramenta de cobertura de testes pega os binários de teste wasm compilados e os converte a arquivos wat, que podem então ser facilmente vistoriados para ver se os handlers definidos em subgraph.yaml foram chamados ou não. Como a cobertura de código (e os testes em geral) está em estado primitivo no AssemblyScript e WebAssembly, o Matchstick não pode procurar por coberturas de branch. Em vez disto, presumimos que se um handler foi chamado, o evento/a função correspondente já passou por testes com êxito.

Pré-requisitos

Link para esta seção

Para executar a funcionalidade da cobertura de teste providenciada no Matchstick, precisa preparar algumas coisas com antecedência:

Exportar seus handlers

Link para esta seção

Para que o Matchstick confira quais handlers são executados, estes handlers devem ser exportados do arquivo de teste primeiro. No nosso exemplo, temos o seguinte handler a ser importado no nosso arquivo gravity.test.ts:

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

Para que essa função seja visível (para ser incluída no arquivo wat por nome) também precisamos exportá-la assim:

export { handleNewGravatar }

Assim que tudo estiver pronto, para executar a ferramenta de cobertura de testes, basta:

graph test -- -c

Também pode adicionar um comando coverage personalizado ao seu arquivo package.json, assim:

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

Isto executará a ferramenta de cobertura. Verás algo parecido com isto no 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).

Duração do teste na saída do log

Link para esta seção

A saída do log inclui a duração do teste. Veja um exemplo:

[Thu, 31 Mar 2022 13:54:54 +0300] Program executed in: 42.270ms.

Erros comuns do compilador

Link para esta seção

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

Isto significa que usou o console.log no seu código, que não é apoiado pelo AssemblyScript. Considere usar a API de Logging

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)

A diferença nos argumentos é causada pela diferença no graph-ts e no matchstick-as. Problemas como este são melhor resolvidos ao atualizar tudo para a versão mais recente.

Caso tenha qualquer pergunta, opinião, pedidos de recursos, ou só quer entrar em contacto, venha para o Discord do The Graph — lá, temos um canal dedicado ao Matchstick, chamado 🔥| unit-testing.

Editar página

Anterior
Problemas Comuns no AssemblyScript
Próximo
Perguntas Frequentes dos Programadores
Editar página