Enhetsprovningsramverk
Reading time: 25 min
Matchstick är ett enhetsprovningsramverk utvecklat av 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!
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
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 install
igen 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).
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 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 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/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
Du kan prova och leka med exemplen från den här guiden genom att klona
Du kan också kolla på videoserien om
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 Gravatarassert.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/0x0assert.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 Gravatarassert.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/0x0assert.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 Gravatarassert.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/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',)
Låt oss se hur ett enkelt enhetstest skulle se ut med hjälp av Gravatar-exemplen i .
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.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}
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ändelserlet newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')// Anropa mappningsfunktioner som skickar händelserna vi just skapadehandleNewGravatars([newGravatarEvent, anotherGravatarEvent])// Bekräfta butikens tillståndassert.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 startclearStore()})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 . 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 funktionencreateNewGravatarEvent()
. - 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:
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()
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)
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);});}
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()
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 denexport { 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 callbackexport function processGravatar(value: JSONValue, userData: Value): void {// Se JSONValue-dokumentationen för mer information om hur man hanterar// med JSON-värdenlet obj = value.toObject()let id = obj.get('id')if (!id) {return}// Callbacks kan också skapa enheterlet gravatar = new Gravatar(id.toString())gravatar.displayName = userData.toString() + id.toString()gravatar.save()}// funktion som anropar 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()}
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.
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äslet logType = newGravatarEvent.logType// Skrivlet UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));
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')
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)
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.
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)})})
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.
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))})
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 wat
filer, 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ör att köra testtäckningsfunktionaliteten som tillhandahålls i Matchstick måste du förbereda några saker i förväg:
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 -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).
Loggutmatningen innehåller testkörningens varaktighet. Här är ett exempel:
[Thu, 31 Mar 2022 13:54:54 +0300] Program executed in: 42.270ms.
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
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.