Cadre pour les tests unitaires
Reading time: 27 min
Matchstick est un cadre de test unitaire, développé par , qui permet aux développeurs de subgraphs de tester leur logique de cartographie dans un environnement de type bac à sable et de déployer leurs subgraphs en toute confiance !
Pour utiliser les méthodes d'assistance aux tests et exécuter les tests, vous devrez installer les dépendances suivantes :
yarn add --dev matchstick-as
❗ graph-node
dépend de PostgreSQL, donc si vous ne l'avez pas déjà, vous devrez l'installer. Nous vous conseillons vivement d'utiliser les commandes ci-dessous, car l'ajouter d'une autre manière peut provoquer des erreurs inattendues !
Commande d'installation Postgres :
brew install postgresql
Créez un lien symbolique vers la dernière libpq.5.lib Vous devrez peut-être d'abord créer ce répertoire /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
Commande d'installation de Postgres (dépend de votre distribution) :
sudo apt installer postgresql
Vous pouvez utiliser Matchstick sur WSL en utilisant à la fois l'approche Docker et l'approche binaire. Comme WSL peut être un peu délicat, voici quelques conseils au cas où vous rencontreriez des problèmes tels que
static BYTES = Symbol("Bytes") SyntaxError: Unexpected token =
ou bien
<PROJECT_PATH>/node_modules/gluegun/build/index.js:13 throw up;
Veuillez vous assurer que vous utilisez une version plus récente de Node.js graph-cli qui ne prend plus en charge la v10.19.0, et qu'il s'agit toujours de la version par défaut pour le nouvel Ubuntu. images sur WSL. Par exemple, il est confirmé que Matchstick fonctionne sur WSL avec v18.1.0, vous pouvez y accéder soit via nvm ou si vous mettez à jour votre Node.js global. N'oubliez pas de supprimer node_modules
et d'exécuter à nouveau npm install
après avoir mis à jour votre nodejs ! Ensuite, assurez-vous que libpq est installé, vous pouvez le faire en exécutant
sudo apt-get install libpq-dev
Et en conclussion, n'utilisez pas graph test
(qui utilise votre installation globale de graph-cli et pour une raison quelconque, cela semble être cassé sur WSL actuellement), utilisez plutôt yarn test
ou npm run test
(cela utilisera l'instance locale de graph-cli au niveau du projet, qui fonctionne à merveille). Pour cela, vous devez bien sûr avoir un script "test"
dans votre fichier package.json
qui peut être quelque chose d'aussi simple que
{"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"}}
Pour utiliser Matchstick dans votre projet de subgraph, il suffit d'ouvrir un terminal, de naviguer vers le dossier racine de votre projet et d'exécuter simplement graph test [options] <datasource>
- il télécharge le dernier binaire Matchstick et exécute le test spécifié ou tous les tests dans un dossier de test (ou tous les tests existants si aucun datasource flag n'est spécifié).
Cette opération permet d'exécuter tous les tests contenus dans le dossier test :
graph test
Ceci lancera un test nommé gravity.test.ts et/ou tous les tests à l'intérieur d'un dossier nommé gravity :
gravity graph test
Ce fichier de test sera le seul à être exécuté :
graph test path/to/file.test.ts
Les Options :
-c, --coverage Exécuter les tests en mode couverture-d, --docker Exécutez les tests dans un conteneur Docker (Remarque : veuillez exécuter à partir du dossier racine du subgraph)-f, --force Binary : télécharge à nouveau le binaire. Docker : télécharge à nouveau le fichier Docker et reconstruit l'image Docker.-h, --help Afficher les informations d'utilisation-l, --logs Enregistre dans la console des informations sur le système d'exploitation, le modèle de processeur et l'URL de téléchargement (à des fins de débogage)-r, --recompile Force les tests à être recompilés-v, --version <tag> Choisissez la version du binaire Rust que vous souhaitez télécharger/utiliser
À partir de graph-cli 0.25.2
, la commande graph test
prend en charge l'exécution de matchstick
dans un conteneur Docker avec le -d</0. > drapeau. L'implémentation de Docker utilise <a href="https://docs.docker.com/storage/bind-mounts/">bind mount</a> afin de ne pas avoir à reconstruire l'image Docker à chaque fois que la commande <code>graph test -d
est exécutée. Vous pouvez également suivre les instructions du référentiel pour exécuter Docker manuellement.
❗ graph test -d
forces docker run
to run with flag -t
. This must be removed to run inside non-interactive environments (like GitHub CI).
❗ En cas d'exécution préalable de graph test
, vous risquez de rencontrer l'erreur suivante lors de la construction de docker :
error from sender: failed to xattr node_modules/binary-install-raw/bin/binary-<platform>: permission denied
Dans ce cas, il faut créer un .dockerignore
dans le dossier racine et ajoutez node_modules/binary-install-raw/bin
Matchstick peut être configuré pour utiliser des tests personnalisés, des bibliothèques et un chemin de manifeste via le fichier de configuration matchstick.yaml
:
testsFolder: path/to/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
Vous pouvez tester et jouer avec les exemples de ce guide en clonant le repo
Vous pouvez également consulter la série de vidéos sur
IMPORTANT: The test structure described below depens on matchstick-as
version >=0.5.0
describe(name: String , () => {})
- Définit un groupe de test.
Notez :
- Les descriptions ne sont pas indispensable. Vous pouvez toujours utiliser test() à l'ancienne, en dehors des blocs describe()
L'exemple:
import { describe, test } from "matchstick-as/assembly/index"import { handleNewGravatar } from "../../src/gravity"describe("handleNewGravatar()", () => {test("Devrait créer une nouvelle entité Gravatar", () => {...})})
Exemple de décrire ()
imbriqué :
import { describe, test } from "matchstick-as/assembly/index"import { handleUpdatedGravatar } from "../../src/gravity"describe("handleUpdatedGravatar()", () => {describe("Lorsque l'entité existe", () => {test("met à jour l'entité", () => {...})})describe("Lorsque l'entité n'existe pas", () => {test("il crée une nouvelle entitéy", () => {...})})})
test(name: String, () =>, Should_fail: bool)
- Définit un scénario de test. Vous pouvez utiliser test() à l’intérieur des blocs décrire() ou indépendamment.
L'exemple:
import { describe, test } from "matchstick-as/assembly/index"import { handleNewGravatar } from "../../src/gravity"describe("handleNewGravatar()", () => {test("Devrait créer une nouvelle entité", () => {...})})
ou bien
test("handleNewGravatar() doit créer une nouvelle entité", () => {...})
Exécute un bloc de code après tous les tests du fichier. Si afterAll
est déclaré à l'intérieur d'un bloc describe
, il est exécuté à la fin de ce bloc describe
.
Les Exemples:
Le code contenu dans beforeAll
sera exécuté une fois avant les tests all du fichier.
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("Lorsque l'entité n'existe pas", () => {test("il devrait créer un nouveau Gravatar avec l'id 0x1", () => {...})})describe("Lorsque l'entité existe déjà", () => {test("il devrait mettre à jour le Gravatar avec l'id 0x0", () => {...})})
Le code contenu dans beforeAll
sera exécuté une fois avant tous les tests du premier bloc de description
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 = "Premier Gravatar"gravatar.save()...})test("met à jour le Gravatar avec l'identifiant 0x0", () => {...})test("crée un nouveau Gravatar avec l'identifiant 0x1", () => ; {...})})
Lance un bloc de code après tous les tests du fichier. Si afterAll
est déclaré à l'intérieur d'un bloc describe
, il s'exécute à la fin de ce bloc describe
.
L'exemple:
Le code situé dans afterAll
sera exécuté une fois après all tests dans le fichier.
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("crée un Gravatar avec l'identifiant 0x0", () => {...})})describe("handleUpdatedGravatar", () => {test("met à jour Gravatar avec l'identifiant 0x0", () => {...})})
Le code à l'intérieur de afterAll
s'exécute une fois après tous les tests du premier bloc de description
import { describe, test, afterAll, clearStore } from "matchstick-as/assembly/index"import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"describe("handleNewGravatar", () => {afterAll(() => {store.remove("Gravatar", "0x1")...})test("Crée une nouvelle entité avec l'identifiant 0x0", () => {...})test("Crée une nouvelle entité avec l'identifiant 0x1", () => {...})})describe("handleUpdatedGravatar", () => {test("Met à jour le Gravatar avec l'identifiant 0x0", () => {...})})
Lance un bloc de code avant chaque test. Si beforeEach
est déclaré à l'intérieur d'un bloc describe
, il s'exécute avant chaque test de ce bloc describe
.
Exemples : Le code contenu dans beforeEach
s'exécute avant chaque test.
importez { 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", () => {...})})...
Exécutez un bloc de code avant chaque test. Si beforeEach
est déclaré à l'intérieur d'un bloc describe, il s'exécute avant chaque test de ce bloc 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 qui devrait mettre à jour le nom d'affichage à 1 Gravatarassert.fieldEquals('Gravatar', '0x0', 'displayName', '1st Gravatar')store.remove('Gravatar', '0x0')})test('Updates the imageUrl', () => {assert.fieldEquals('Gravatar', '0x0', 'imageUrl', '')// code qui devrait changer le imageUrl en https://www.gravatar.com/avatar/0x0assert.fieldEquals('Gravatar', '0x0', 'imageUrl', 'https://www.gravatar.com/avatar/0x0')store.remove('Gravatar', '0x0')})})
Lance un bloc de code après chaque test. Si afterEach
est déclaré à l'intérieur d'un bloc de description
, il s'exécute après chaque test de ce bloc de description
.
Les Exemples:
Le code dans afterEach
sera exécuté après chaque 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")// code qui devrait mettre à jour le nom d'affichage à 1 Gravatarassert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")})test("Updates the imageUrl", () => {assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")// code qui devrait changer le imageUrl en https://www.gravatar.com/avatar/0x0assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")})})
Le code contenu dans afterEach
exécutera après chaque test dans cette description
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 qui devrait mettre à jour le nom d'affichage à 1 Gravatarassert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")})test("Updates the imageUrl", () => {assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")// code qui devrait changer le imageUrl en 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',)
Voyons à quoi ressemblerait un test unitaire simple en utilisant les exemples Gravatar dans le .
En supposant que nous disposions de la fonction de traitement suivante (ainsi que de deux fonctions d'aide pour nous faciliter la vie) :
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}
Nous devons tout d'abord créer un fichier de test dans notre projet. Voici un exemple de ce à quoi cela pourrait ressembler :
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('Peut appeler des mappings avec des événements personnalisés', () => {// Crée une entité de test et l'enregistre dans le store comme état initial (optionnel)let gravatar = new Gravatar('gravatarId0')gravatar.save()// Crée des événements facticeslet newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')// Appelle les fonctions de mapping en passant les événements qu'on vient de créerhandleNewGravatars([newGravatarEvent, anotherGravatarEvent])// Vérifie l'état du storeassert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')// Vide le store afin de commencer le prochain test avec un état propreclearStore()})test('Test suivant', () => {//...})
Cela fait beaucoup à décortiquer ! Tout d'abord, une chose importante à noter est que nous importons des choses de matchstick-as
, notre bibliothèque d'aide AssemblyScript (distribuée en tant que module npm). Vous pouvez trouver le dépôt . matchstick-as
nous fournit des méthodes de test utiles et définit également la fonction test()
que nous utiliserons pour construire nos blocs de test. Le reste est assez simple - voici ce qui se passe :
- Mettons en place notre état initial et ajoutons une entité Gravatar personnalisée ;
- Définissons deux objets événement
NewGravatar
avec leurs données, en utilisant la fonctioncreateNewGravatarEvent()
; - Appelons des méthodes de gestion pour ces événements -
handleNewGravatars()
et nous passons la liste de nos événements personnalisés ; - Affirmons l'état du magasin. Comment cela fonctionne-t-il ? - Nous passons une combinaison unique de type d'entité et d'identifiant. Ensuite, nous vérifions un champ spécifique de cette entité et affirmons qu'il a la valeur que nous attendons. Nous faisons cela à la fois pour l'entité Gravatar initiale que nous avons ajoutée au magasin, ainsi que pour les deux entités Gravatar qui sont ajoutées lorsque la fonction de gestion est appelée ;
- Et enfin, Nettoyons le magasin à l'aide de
clearStore()
afin que notre prochain test puisse commencer avec un objet magasin frais et vide. Nous pouvons définir autant de blocs de test que nous le souhaitons.
Et voilà, nous avons formulé notre premier test ! 👏
Maintenant, afin d'exécuter nos tests, il suffit d'exécuter ce qui suit dans le dossier racine de votre subgraph :
gravity graph test
Et si tout se passe bien, vous devriez être accueilli par ce qui suit :
Les utilisateurs peuvent hydrater le magasin avec un ensemble connu d'entités. Voici un exemple pour initialiser la boutique avec une entité Gravatar :
laissez gravatar = new Gravatar('entryId')gravatar.save()
Un utilisateur peut créer un événement personnalisé et le transmettre à une fonction de cartographie liée au magasin :
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)
Les utilisateurs peuvent appeler les mappages avec des dispositifs de test.
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);});}
Les utilisateurs peuvent simuler des appels de contrat :
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))
Comme démontré, afin de se moquer d'un appel de contrat et d'obtenir une valeur de retour, l'utilisateur doit fournir une adresse de contrat, un nom de fonction, une signature de fonction, un tableau d'arguments et bien sûr – la valeur de retour.
Utilisateurs peuvent également simuler des annulations de fonctions :
laissez contractAddress = Address.fromString('0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')createMockedFunction(contractAddress, 'getGravatar', 'getGravatar(address):(string,string)').withArgs([ethereum.Value.fromAddress(contractAddress)]).reverts()
Les utilisateurs peuvent simuler les fichiers IPFS en utilisant la fonction mockIpfsFile(hash, filePath)
. La fonction accepte deux arguments, le premier est le hachage/chemin du fichier IPFS et le second est le chemin d'accès à un fichier local.
NOTEZ : Lorsque vous testez ipfs.map/ipfs.mapJSON
, la fonction de rappel doit être exportée du fichier de test afin que matchstck puisse la détecter, comme la fonction processGravatar()
dans l'exemple de test ci-dessous :
Fichier .test.ts
:
import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'import { ipfs } from '@graphprotocol/graph-ts'import { gravatarFromIpfs } from './utils'// Exportation du callback ipfs.map() pour que matchstck le détecte.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')})
Fichier utils.ts
:
import { Address, ethereum, JSONValue, Value, ipfs, json, Bytes } from "@graphprotocol/graph-ts"import { Gravatar } from "../../generated/schema"...// rappel ipfs.mapexport function processGravatar(value: JSONValue, userData: Value): void {// Consultez la documentation de JSONValue pour plus de détails sur la façon de traiter les données.// avec JSON valueslet obj = value.toObject()let id = obj.get('id')if (!id) {return}// Des entités de rappel peuvent également être crééeslet gravatar = new Gravatar(id.toString())gravatar.displayName = userData.toString() + id.toString()gravatar.save()}// fonction qui appelle 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()}
Les utilisateurs sont en mesure d'affirmer l'état final (ou intermédiaire) du magasin via des entités d'affirmation. Pour ce faire, l'utilisateur doit fournir un type d'entité, l'ID spécifique d'une entité, le nom d'un champ sur cette entité et la valeur attendue du champ. Voici un exemple rapide:
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')
L'exécution de la fonction assert.fieldEquals() vérifiera l'égalité du champ donné par rapport à la valeur attendue indiquée. Le test échouera et un message d'erreur sera généré si les valeurs sont NON égales. Sinon, le test réussira.
Les utilisateurs peuvent utiliser les métadonnées de transaction par défaut, qui peuvent être renvoyées comme un ethereum. Event en utilisant la fonction newMockEvent()
. L'exemple suivant montre comment vous pouvez lire/écrire dans ces champs sur l'objet Event :
// Lisezlet logType = newGravatarEvent.logType// Écrivezlet UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)
assert.equals(ethereum.Value.fromString("bonjour"); ethereum.Value.fromString("bonjour"));
Les utilisateurs peuvent affirmer qu'une entité n'existe pas dans le magasin. La fonction prend un type d'entité et un identifiant. Si l'entité se trouve effectivement dans le magasin, le test échouera avec un message d'erreur pertinent. Voici un exemple rapide de la façon d'utiliser cette fonctionnalité :
assert.notInStore('Gravatar', '23')
Vous pouvez imprimer l'intégralité du magasin sur la console à l'aide de cette fonction d'assistance:
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)
Les utilisateurs peuvent s'attendre à des échecs de test, en utilisant l'indicateur ShouldFail sur les fonctions test() :
test('Devrait générer une erreur',() => {lancer une nouvelle erreur()},vrai,)
Si le test est marqué avec ShouldFail = true mais n'échoue PAS, cela apparaîtra comme une erreur dans les journaux et le bloc de test échouera. De plus, s'il est marqué avec ShouldFail = false (l'état par défaut), l'exécuteur de test plantera.
Avoir des journaux personnalisés dans les tests unitaires équivaut exactement à la journalisation des mappages. La différence est que l'objet journal doit être importé depuis matchstick-as plutôt que graph-ts. Voici un exemple simple avec tous les types de journaux non critiques :
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!", []);});
Les utilisateurs peuvent également simuler une panne critique, comme ceci :
test('Tout faire exploser', () => {log.critical('Boom!')})
La journalisation des erreurs critiques arrêtera l’exécution des tests et fera tout exploser. Après tout, nous voulons nous assurer que votre code ne contient pas de journaux critiques lors du déploiement, et vous devriez le remarquer immédiatement si cela devait se produire.
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)})})
Le test des sources de données dynamiques peut être effectué en simulant la valeur de retour des fonctions context()
, address()
et network()
du Espace de noms dataSource. Ces fonctions renvoient actuellement les éléments suivants : context()
- renvoie une entité vide (DataSourceContext), address()
- renvoie 0x000000000000000000000000000000000000000000
, network()
- renvoie mainnet
. Les fonctions create(...)
et createWithContext(...)
sont simulées pour ne rien faire, elles n'ont donc pas du tout besoin d'être appelées dans les tests. Les modifications des valeurs de retour peuvent être effectuées via les fonctions de l'espace de noms dataSourceMock
dans matchstick-as
(version 0.3.0+).
L'exemple ci-dessous :
Nous avons d’abord le gestionnaire d’événements suivant (qui a été intentionnellement réutilisé pour présenter la moquerie de la source de données) :
fonction d'exportation handleApproveTokenDestinations (événement : ApproveTokenDestinations) : void {laissez tokenLockWallet = TokenLockWallet.load(dataSource.address().toHexString()) !if (dataSource.network() == 'rinkeby') {tokenLockWallet.tokenDestinationsApproved = true}laissez contexte = dataSource.context()if (context.get('contextVal')!.toI32() > 0) {tokenLockWallet.setBigInt('tokensReleased', BigInt.fromI32(context.get('contextVal')!.toI32()))}tokenLockWallet.save()}
Et puis nous avons le test utilisant l'une des méthodes de l'espace de noms dataSourceMock pour définir une nouvelle valeur de retour pour toutes les fonctions dataSource :
importer { assert, test, newMockEvent, dataSourceMock } depuis 'matchstick-as/assembly/index'importer { BigInt, DataSourceContext, Value } depuis '@graphprotocol/graph-ts'importer { handleApproveTokenDestinations } depuis '../../src/token-lock-wallet'importer { ApproveTokenDestinations } depuis '../../generated/templates/GraphTokenLockWallet/GraphTokenLockWallet'importer { TokenLockWallet } depuis '../../generated/schema'test('Exemple moqueur simple de source de données', () => {laissez adresseString = '0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'let adresse = Adresse.fromString(addressString)laissez wallet = new TokenLockWallet (address.toHexString())portefeuille.save()laisser contexte = new DataSourceContext()contexte.set('contextVal', Value.fromI32(325))dataSourceMock.setReturnValues(addressString, 'rinkeby', contexte)let event = changetype<ApproveTokenDestinations>(newMockEvent())assert.assertTrue(!wallet.tokenDestinationsApproved)handleApproveTokenDestinations (événement)portefeuille = TokenLockWallet.load(address.toHexString()) !assert.assertTrue(wallet.tokenDestinationsApproved)assert.bigIntEquals(wallet.tokensReleased, BigInt.fromI32(325))dataSourceMock.resetValues()})
Notez que dataSourceMock.resetValues() est appelé à la fin. C'est parce que les valeurs sont mémorisées lorsqu'elles sont modifiées et doivent être réinitialisées si vous voulez revenir aux valeurs par défaut.
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))})
Grâce à Matchstick, les développeurs de subgraphs peuvent exécuter un script qui calculera la couverture des tests unitaires écrits.
L'outil de couverture de test prend les binaires de test wasm
compilés et les convertit en fichiers wat
, qui peuvent ensuite être facilement inspectés pour voir si les gestionnaires définis dans subgraph .yaml
ont été appelés. Étant donné que la couverture du code (et les tests dans leur ensemble) en sont à leurs tout premiers stades dans AssemblyScript et WebAssembly, Matchstick ne peut pas vérifier la couverture des branches. Au lieu de cela, nous nous appuyons sur l'affirmation selon laquelle si un gestionnaire donné a été appelé, l'événement/la fonction correspondant a été correctement simulé.
Pour exécuter la fonctionnalité de couverture de test fournie dans Matchstick, vous devez préparer quelques éléments au préalable :
Pour que Matchstick vérifie quels gestionnaires sont exécutés, ces gestionnaires doivent être exportés à partir du fichier de test. Ainsi, par exemple, dans notre exemple, dans notre fichier gravitation.test.ts, nous avons le gestionnaire suivant en cours d'importation :
importez { handleNewGravatar } from '../../src/gravity'
Pour que cette fonction soit visible (pour qu'elle soit incluse dans le fichier wat
par son nom), nous devons également l'exporter, comme ceci :
exportez { handleNewGravatar }
Une fois tout configuré, pour exécuter l'outil de couverture de test, exécutez simplement :
graph test -- -c
Vous pouvez également ajouter une commande coverage
personnalisée à votre fichier package.json
, comme ceci :
"scripts": {/.../"coverage": "test graph -- -c"},
Cela exécutera l'outil de couverture et vous devriez voir quelque chose comme ceci dans le terminal :
$ graph test -cSauter l'étape de téléchargement/installation car le binaire existe déjà à l'adresse suivante : /Users/petko/work/demo-subgraph/node_modules/binary-install-raw/bin/0.4.0___ ___ _ _ _ _ _| \/ | | | | | | | (_) | || . . | __ _| |_ ___| |__ ___| |_ _ ___| | __| |\/| |/ _` | __/ __| '_ \/ __| __| |/ __| |/ /| | | | (_| | || (__| | | \__ \ |_| | (__| <\_| |_/\__,_|\__\___|_| |_|___/\__|_|\___|_|\_\Compilation...Exécution en mode rapport de couverture.️Lecture des modules de test générés... 🔎️Génération du rapport de couverture 📝Handlers for source 'Gravity':Handler 'handleNewGravatar' is tested.Handler 'handleUpdatedGravatar' is not tested.Handler 'handleCreateGravatar' is tested.Test coverage: 66.7% (2/3 handlers).Handlers for source 'GraphTokenLockWallet':Handler 'handleTokensReleased' is not tested.Handler 'handleTokensWithdrawn' is not tested.Handler 'handleTokensRevoked' is not tested.Handler 'handleManagerUpdated' is not tested.Handler 'handleApproveTokenDestinations' is not tested.Handler 'handleRevokeTokenDestinations' is not tested.Test coverage: 0.0% (0/6 handlers).Global test coverage: 22.2% (2/9 handlers).
La sortie du journal inclut la durée de l’exécution du test. Voici un exemple :
[Jeudi 31 mars 2022 13:54:54 +0300] Programme exécuté en : 42,270 ms.
Critique : impossible de créer WasmInstance à partir d'un module valide avec un contexte : importation inconnue : wasi_snapshot_preview1::fd_write n'a pas été défini
Cela signifie que vous avez utilisé console.log
dans votre code, ce qui n'est pas pris en charge par AssemblyScript. Veuillez envisager d'utiliser l'
ERREUR TS2554 : attendu ? arguments, mais j'ai eu ?.
renvoyer le nouveau ethereum.Block (defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultAddress, defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt) ;
dans ~lib/matchstick-as/assembly/defaults.ts(18,12)
ERROR TS2554: Expected ? arguments, but got ?.
renvoyer un nouveau ethereum.Transaction (defaultAddressBytes, defaultBigInt, defaultAddress, defaultAddress, defaultBigInt, defaultBigInt, defaultBigInt, defaultAddressBytes, defaultBigInt) ;
dans ~lib/matchstick-as/assembly/defaults.ts(24,12)
L'inadéquation des arguments est causée par une inadéquation entre graph-ts
et matchstick-as
. La meilleure façon de résoudre des problèmes comme celui-ci est de tout mettre à jour vers la dernière version publiée.
Si vous avez des questions, des commentaires, des demandes de fonctionnalités ou si vous souhaitez simplement nous contacter, le meilleur endroit serait The Graph Discord où nous avons une chaîne dédiée à Matchstick, appelée 🔥| tests unitaires.