Developing > Enhetsprovningsramverk

Enhetsprovningsramverk

Reading time: 25 min

Matchstick är ett enhetsprovningsramverk utvecklat av LimeChain som möjliggör för subgraph-utvecklare att testa sin kartläggningslogik i en avskärmad miljö och distribuera sina subgrapher med förtroende!

Komma igång

Länk till detta avsnitt

Installera beroenden

Länk till detta avsnitt

För att använda testhjälpmedlen och köra testerna måste du installera följande beroenden:

yarn add --dev matchstick-as

❗ graph-node är beroende av PostgreSQL, så om du inte redan har det måste du installera det. Vi rekommenderar starkt att du använder följande kommandon eftersom att lägga till det på något annat sätt kan orsaka oväntade fel!

Kommando för installation av Postgres:

brew install postgresql

Skapa en symbolisk länk till den senaste libpq.5.lib._ Du kanske behöver skapa den här mappen först: _/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

Kommando för Postgres installation (beroende på din distribution):

sudo apt install postgresql

WSL (Windows Subsystem for Linux)

Länk till detta avsnitt

Du kan använda Matchstick i WSL både med Docker-metoden och binärmetoden. Eftersom WSL kan vara lite knepigt, här är några tips om du stöter på problem som

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

eller

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

Se till att du använder en nyare version av Node.js eftersom graph-cli inte längre stöder v10.19.0, och det är fortfarande standardversionen för nya Ubuntu-bilder på WSL. Till exempel är Matchstick bekräftat fungerande på WSL med v18.1.0. Du kan byta till den antingen via** nvm ** eller genom att uppdatera din globala Node.js. Glöm inte att ta bort node_modules och köra npm installigen efter att du har uppdaterat Node.js! Sedan, se till att du har libpq installerat, du kan göra det genom att köra

sudo apt-get install libpq-dev

Och till sist, använd inte graph test (som använder din globala installation av graph-cli och av någon anledning ser ut som om det är trasig på WSL för närvarande), istället använd yarn test eller npm run test (det kommer att använda den lokala projektbaserade instansen av graph-cli, som fungerar utmärkt). För detta behöver du självklart ha ett "test"-skript i din package.json-fil, vilket kan vara något så enkelt som

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

För att använda Matchstick i ditt subgrafprojekt öppnar du bara en terminal, navigerar till rotmappen för ditt projekt och kör helt enkelt graftest [options] <datasource> - den laddar ner den senaste Matchstick-binären och kör det angivna testet eller alla tester i en testmapp (eller alla befintliga tester om ingen datakällasflagga är angiven).

CLI alternativ

Länk till detta avsnitt

Detta kommer att köra alla tester i testmappen:

graph test

Detta kommer att köra en test med namnet gravity.test.ts och/eller alla tester inuti en mapp med namnet gravity:

graph test gravity

Då körs endast den specifika testfilen:

graph test path/to/file.test.ts

Alternativ:

-c, --coverage Kör testerna i täckningsläge
-d, --docker Kör testerna i en docker-container (Observera: Kör från rotmappen för subgraph)
-f, --force Binär: Hämtar om binären. Docker: Hämtar om Dockerfilen och bygger om dockerbilden.
-h, --help Visar användningsinformation
-l, --logs Loggar till konsolen information om OS, CPU-modell och nedladdnings-URL (för felsökningssyften)
-r, --recompile Tvingar testerna att kompileras om
-v, --version <tag> Välj versionen av den rust binära som du vill att den ska hämtas/användas

Från graph-cli 0.25.2 stöder kommandot graph test att köra matchstick i en Docker-behållare med flaggan -d. Docker-implementeringen använder bind mount så att den inte behöver bygga om dockerbilden varje gång kommandot graph test -d körs. Alternativt kan du följa instruktionerna från matchstick repository för att köra Docker manuellt.

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

❗ Om du tidigare har kört graph test kan du stöta på följande fel under docker build:

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

I det här fallet skapar du en .dockerignore i rotmappen och lägger till node_modules/binary-install-raw/bin.

Matchstick kan konfigureras att använda en anpassad sökväg för tester, libs och manifest via konfigurationsfilen matchstick.yaml:

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

Demo undergraf

Länk till detta avsnitt

Du kan prova och leka med exemplen från den här guiden genom att klona Demo Subgraph-repot

Handledning för video

Länk till detta avsnitt

Du kan också kolla på videoserien om "Hur man använder Matchstick för att skriva enhetstester för dina subgraph"

Tests structure

Länk till detta avsnitt

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

describe(name: String , () => {}) - Definierar en testgrupp.

Noteringar:

  • Describes är inte obligatoriska. Du kan fortfarande använda test() på det gamla sättet, utanför describe() blocken

Exempel:

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

Nästat describe() exempel:

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) - Definierar ett testfall. Du kan använda test() inuti describe()-block eller fristående.

Exempel:

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

eller

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

Kör en kodblock före något av testen i filen. Om beforeAll deklareras inuti en describe-block körs den i början av det describe-blocket.

Exempel:

Kod inuti beforeAll kommer att utföras en gång före alla tester i filen.

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("När enheten inte existerar", () => {
test("det bör skapa en ny Gravatar med id 0x1", () => {
...
})
})
describe("När enheten redan existerar", () => {
test("det bör uppdatera Gravatar med id 0x0", () => {
...
})
})

Kod inuti beforeAll kommer att exekveras en gång före alla tester i det första beskrivningsblocket

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("uppdaterar Gravatar med id 0x0", () => {
...
})
test("skapar ny Gravatar med id 0x1", () => {
...
})
})

Kör en kodblock efter alla test i filen. Om afterAll deklareras inuti en describe-block körs den i slutet av det describe-blocket.

Exempel:

Kod inuti afterAll kommer att utföras en gång efter alla tester i filen.

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("skapar Gravatar med id 0x0", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("uppdaterar Gravatar med id 0x0", () => {
...
})
})

Kod inuti afterAll kommer att exekveras en gång efter alla tester i det första beskrivna blocket

import { describe, test, afterAll, clearStore } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar", () => {
afterAll(() => {
store.remove("Gravatar", "0x1")
...
})
test("Det skapar en ny enhet med id 0x0", () => {
...
})
test("Det skapar en ny enhet med id 0x1", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("uppdaterar Gravatar med id 0x0", () => {
...
})
})

Kör en kodblock före varje test. Om beforeEach deklareras inuti en describe-block körs den före varje test i det describe-blocket.

Exempel: Koden inuti beforeEach kommer att utföras före varje test.

import { describe, test, beforeEach, clearStore } from "matchstick-as/assembly/index"
import { handleNewGravatars } from "./utils"
beforeEach(() => {
clearStore() // <-- rensa butiken före varje test i filen
})
describe("handleNewGravatars, () => {
test("Ett test som kräver en ren butik", () => {
...
})
test("Andra som kräver en ren butik", () => {
...
})
})
...

Kod inuti beforeEach kommer att exekveras endast före varje test i den som beskriver

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 = 'Första Gravatar'
gravatar.imageUrl = ''
gravatar.save()
})
test('Upates the displayName', () => {
assert.fieldEquals('Gravatar', '0x0', 'displayName', 'First Gravatar')
// kod som ska uppdatera displayName till 1st Gravatar
assert.fieldEquals('Gravatar', '0x0', 'displayName', '1st Gravatar')
store.remove('Gravatar', '0x0')
})
test('Updates the imageUrl', () => {
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', '')
// kod som ska ändra imageUrl till https://www.gravatar.com/avatar/0x0
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', 'https://www.gravatar.com/avatar/0x0')
store.remove('Gravatar', '0x0')
})
})

Kör en kodblock efter varje test. Om afterEach deklareras inuti en describe-block körs den efter varje test i det describe-blocket.

Exempel:

Kod inuti afterEach kommer att utföras efter varje test.

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")
// kod som ska uppdatera displayName till 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// kod som ska ändra imageUrl till https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})

Kod i afterEach kommer att exekveras efter varje test i den beskrivningen

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")
// kod som ska uppdatera displayName till 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// kod som ska ändra imageUrl till https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})
fieldEquals(entityType: string, id: string, fieldName: string, expectedVal: string)
equals(expected: ethereum.Value, actual: ethereum.Value)
notInStore(entityType: string, id: string)
addressEquals(address1: Address, address2: Address)
bytesEquals(bytes1: Bytes, bytes2: Bytes)
i32Equals(number1: i32, number2: i32)
bigIntEquals(bigInt1: BigInt, bigInt2: BigInt)
booleanEquals(bool1: boolean, bool2: boolean)
stringEquals(string1: string, string2: string)
arrayEquals(array1: Array<ethereum.Value>, array2: Array<ethereum.Value>)
tupleEquals(tuple1: ethereum.Tuple, tuple2: ethereum.Tuple)
assertTrue(value: boolean)
assertNull<T>(value: T)
assertNotNull<T>(value: T)
entityCount(entityType: string, expectedCount: i32)

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

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

Skriv en enhetstest

Länk till detta avsnitt

Låt oss se hur ett enkelt enhetstest skulle se ut med hjälp av Gravatar-exemplen i Demo Subgraph.

Antag att vi har följande hanteringsfunktion (tillsammans med två hjälpfunktioner för att göra vårt liv enklare):

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
}

Vi måste först skapa en testfil i vårt projekt. Det här är ett exempel på hur det kan se ut:

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', () => {
// Skapa en testenhet och spara den i arkivet som initialtillstånd (valfritt)
let gravatar = new Gravatar('gravatarId0')
gravatar.save()
// Skapa låtsashändelser
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
// Anropa mappningsfunktioner som skickar händelserna vi just skapade
handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
// Bekräfta butikens tillstånd
assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')
// Rensa lagret för att starta nästa test med en ny start
clearStore()
})
test('Next test', () => {
//...
})

Det är mycket att ta in! Först och främst är det viktigt att notera att vi importerar saker från matchstick-as, vår AssemblyScript hjälpbibliotek (distribuerat som ett npm-paket). Du kan hitta lagringsplatsen här. matchstick-as förser oss med användbara testmetoder och definierar också funktionen test() som vi kommer att använda för att bygga våra testblock. Resten är ganska självförklarande - här är vad som händer:

  • Vi ställer in vår inledande status och lägger till en anpassad Gravatar-entitet;
  • Vi definierar två NewGravatar händelseobjekt tillsammans med deras data, med hjälp av funktionen createNewGravatarEvent().
  • Vi kallar på våra hanteringsmetoder för dessa händelser - handleNewGravatars() och skickar in listan med våra anpassade händelser;
  • Vi försäkrar oss om statusen för lagringen. Hur fungerar det? - Vi skickar en unik kombination av entitetstyp och id. Sedan kontrollerar vi ett specifikt fält på den entiteten och försäkrar oss om att det har det värde vi förväntar oss. Vi gör detta både för den ursprungliga Gravatar-entiteten vi lade till i lagringen och de två Gravatar-entiteterna som läggs till när hanteringsfunktionen anropas;
  • Och sist men inte minst - vi rensar lagringen med hjälp av clearStore() så att vårt nästa test kan börja med en fräsch och tom lagringsobjekt. Vi kan definiera så många testblock som vi vill.

Så där har vi skapat vårt första test! 👏

För att köra våra tester behöver du helt enkelt köra följande i din subgrafs rotmapp:

graph test Gravity

Och om allt går bra bör du hälsas av följande:

Matchstick säger Alla tester har passerat

Vanliga testscenarier

Länk till detta avsnitt

Fylla på lagringen med en viss status

Länk till detta avsnitt

Användare kan fylla på lagringen med en känd uppsättning entiteter. Här är ett exempel på att initialisera lagringen med en Gravatar-entitet:

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

Anropa en mappnings funktion med en händelse

Länk till detta avsnitt

En användare kan skapa en anpassad händelse och skicka den till en mappningsfunktion som är bunden till butiken:

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)

Anropar alla mappningar med händelsefixturer

Länk till detta avsnitt

Användare kan kalla mappningarna med testfixturer.

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

Mocka kontraktsanrop

Länk till detta avsnitt

Användare kan simulera kontraktssamtal:

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

För att kunna simulera ett kontraktsanrop och ett hardcore returvärde måste användaren tillhandahålla en kontraktsadress, funktionsnamn, funktionssignatur, en uppsättning argument och naturligtvis - returvärdet.

Användare kan också simulera funktionsåtergångar:

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

Simulering av IPFS-filer (från matchstick 0.4.1)

Länk till detta avsnitt

Användare kan simulera IPFS-filer genom att använda funktionen mockIpfsFile(hash, filePath). Funktionen accepterar två argument, det första är IPFS-filens hash/sökväg och det andra är sökvägen till en lokal fil.

OBS: När du testar ipfs.map/ipfs.mapJSON måste callback-funktionen exporteras från testfilen för att matchstck ska upptäcka den, liknande processGravatar()-funktionen i testexemplet nedan:

.test.ts fil:

import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'
import { ipfs } from '@graphprotocol/graph-ts'
import { gravatarFromIpfs } from './utils'
// Exportera ipfs.map() callback så att matchstck kan upptäcka den
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')
})

utils.ts fil:

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 {
// Se JSONValue-dokumentationen för mer information om hur man hanterar
// med JSON-värden
let obj = value.toObject()
let id = obj.get('id')
if (!id) {
return
}
// Callbacks kan också skapa enheter
let gravatar = new Gravatar(id.toString())
gravatar.displayName = userData.toString() + id.toString()
gravatar.save()
}
// funktion som anropar 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()
}

Kontrollera tillståndet för lagret

Länk till detta avsnitt

Användare kan kontrollera det slutgiltiga (eller delvisa) tillståndet för lagret genom att verifiera enheter. För att göra detta måste användaren ange en enhetstyp, den specifika ID: n för en enhet, namnet på ett fält på den enheten och det förväntade värdet på fältet. Här är ett snabbt exempel:

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

Körning av funktionen assert.fieldEquals() kommer att kontrollera om det angivna fältet är lika med det förväntade värdet. Testet kommer att misslyckas och ett felmeddelande kommer att visas om värdena INTE är lika. Annars kommer testet att passera framgångsrikt.

Interagera med händelsemetadata

Länk till detta avsnitt

Användare kan använda standardtransaktionsmetadata, som kan returneras som en ethereum.Event genom att använda funktionen newMockEvent(). Följande exempel visar hur du kan läsa/skriva till de fälten på Event-objektet:

// Läs
let logType = newGravatarEvent.logType
// Skriv
let UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'
newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)

Påstående om variabelns likhet

Länk till detta avsnitt
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

Påstå att en entitet inte finns i butiken

Länk till detta avsnitt

Användare kan hävda att en entitet inte finns i butiken. Funktionen tar en entitetstyp och ett id. Om entiteten faktiskt finns i butiken kommer testet att misslyckas med ett relevant felmeddelande. Här är ett snabbt exempel på hur du använder den här funktionen:

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

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

Länk till detta avsnitt

Du kan skriva ut hela lagret till konsolen med hjälp av denna hjälpfunktion:

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)

Förväntat misslyckande

Länk till detta avsnitt

Användare kan ha förväntade testfel genom att använda flaggan shouldFail på test()-funktionerna:

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

Om testet är markerat med shouldFail = true men INTE misslyckas, kommer det att visas som ett fel i loggarna och testblocket kommer att misslyckas. Om testet är markerat med shouldFail = false (standardtillståndet) kommer testköraren dessutom att krascha.

Att ha anpassade loggar i enhetstesterna är exakt samma sak som att logga i mappningarna. Skillnaden är att loggobjektet måste importeras från matchstick-as snarare än graph-ts. Här är ett enkelt exempel med alla icke-kritiska loggtyper:

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!", []);
});

Användare kan också simulera ett kritiskt fel, t.ex:

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

Loggning av kritiska fel kommer att stoppa utförandet av testerna och orsaka total krasch. Trots allt vill vi säkerställa att din kod inte har kritiska loggar i produktion, och du bör märka det omedelbart om det skulle inträffa.

Testning av härledda fält

Länk till detta avsnitt

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

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

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

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

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

Testing loadInBlock

Länk till detta avsnitt

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

Testning av dynamiska datakällor

Länk till detta avsnitt

Testning av dynamiska datakällor kan göras genom att moka returvärdena för funktionerna context(), address() och network() i dataSource-namespace. Dessa funktioner returnerar för närvarande följande: context() - returnerar en tom entitet (DataSourceContext), address() - returnerar 0x0000000000000000000000000000000000000000, network() - returnerar mainnet. Funktionerna create(...) och createWithContext(...) mokas för att inte göra något, så de behöver inte anropas i testerna alls. Ändringar av returvärden kan göras genom funktionerna i namespace dataSourceMock i matchstick-as (version 0.3.0+).

Exempel nedan:

Först har vi följande händelsehanterare (som medvetet har ändrats för att visa datasourcemockning):

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

Och sedan har vi testet som använder en av metoderna i namespace dataSourceMock för att ställa in ett nytt returvärde för alla dataSource-funktioner:

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

Observera att dataSourceMock.resetValues() anropas i slutet. Det beror på att värdena kom ihåg när de ändrades och behöver återställas om du vill återgå till standardvärdena.

Testing dynamic data source creation

Länk till detta avsnitt

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

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

Testing ethereum/contract templates

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

Testing file/ipfs templates

Länk till detta avsnitt

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

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

Med Matchstick kan subgraph-utvecklare köra ett skript som beräknar täckningen av de skrivna enhetstesterna.

Verktyget för testtäckning tar de kompilerade test wasm binärerna och omvandlar dem till watfiler, som sedan enkelt kan inspekteras för att se om hanterarna som är definierade i subgraph.yaml har blivit kallade eller inte. Eftersom kodtäckning (och tester som helhet) är i mycket tidiga stadier i AssemblyScript och WebAssembly kan Matchstick inte kontrollera grentäckning. Istället förlitar vi oss på påståendet att om en given hanterare har blivit kallad, har händelsen/funktionen för den hanteraren blivit korrekt mockad.

Förutsättningar

Länk till detta avsnitt

För att köra testtäckningsfunktionaliteten som tillhandahålls i Matchstick måste du förbereda några saker i förväg:

Exportera dina hanterare

Länk till detta avsnitt

För att Matchstick ska kunna kontrollera vilka hanterare som körs måste dessa hanterare exporteras från testfilen. Till exempel i vårt exempel, i vår fil gravity.test.ts, har vi följande hanterare som importeras:

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

För att denna funktion skall vara synlig (för att den skall ingå i wat-filen med namn) måste vi också exportera den, så här:

export { handleNewGravatar }

När allt är klart kör du bara testtäckningsverktyget:

graph test -- -c

Du kan också lägga till ett anpassat coverage-kommando i din package.json-fil, så här:

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

Det kommer att köra täckningsverktyget och du bör se något liknande i terminalen:

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

Testkörningens varaktighet i loggutmatningen

Länk till detta avsnitt

Loggutmatningen innehåller testkörningens varaktighet. Här är ett exempel:

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

Vanliga kompilatorfel

Länk till detta avsnitt

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

Det betyder att du har använt console.log i din kod, som inte stöds av AssemblyScript. Överväg att använda Logging API

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)

Motsägelsen i argumenten beror på en motsägelse i graph-ts och matchstick-as. Det bästa sättet att åtgärda problem som detta är att uppdatera allt till den senaste utgivna versionen.

Om du har några frågor, feedback, funktionsförfrågningar eller bara vill nå ut, är det bästa stället The Graph Discord där vi har en dedikerad kanal för Matchstick, kallad 🔥| unit-testing.

Redigera sida

Tidigare
Vanliga problem med AssemblyScript
Nästa
Vanliga frågor för utvecklare
Redigera sida