Cadre pour les tests unitaires
Reading time: 28 min
Learn how to use Matchstick, a unit testing framework developed by . Matchstick enables subgraph developers to test their mapping logic in a sandboxed environment and sucessfully deploy their subgraphs.
- It's written in Rust and optimized for high performance.
- It gives you access to developer features, including the ability to mock contract calls, make assertions about the store state, monitor subgraph failures, check test performance, and many more.
In order to use the test helper methods and run tests, you need to install the following dependencies:
yarn add --dev matchstick-as
graph-node
depends on PostgreSQL, so if you don't already have it, then you will need to install it.
Note: It's highly recommended to use the commands below to avoid unexpected errors.
Installation command:
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
Installation command (depends on your distro):
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
force docker run
à s'exécuter avec le paramètre -t
. Cela doit être supprimé pour s'exécuter dans des environnements non interactifs (comme 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: La structure de test décrite ci-dessous dépend de la version 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)
À partir de la version 0.6.0, les assertions supportent également les messages d'erreur personnalisés
assert.fieldEquals('Gravatar', '0x123', 'id', '0x123', 'L'Id doit être 0x123')assert.equals(ethereum.Value.fromI32(1), ethereum.Value.fromI32(1), 'La valeur doit être égale à 1')assert.notInStore('Gravatar', '0x124', 'Gravatar ne doit pas être dans le magasin')assert.addressEquals(Address.zero(), Address.zero(), 'L'adresse doit être zéro')assert.bytesEquals(Bytes.fromUTF8('0x123'), Bytes.fromUTF8('0x123'), 'Les Bytes doivent être égaux')assert.i32Equals(2, 2, 'I32 doit être égal à 2')assert.bigIntEquals(BigInt.fromI32(1), BigInt.fromI32(1), 'BigInt doit être égal à 1')assert.booleanEquals(true, true, 'Le booléen doit être vrai')assert.stringEquals('1', '1', 'La chaîne de caractère doit être égale à 1')assert.arrayEquals([ethereum.Value.fromI32(1)], [ethereum.Value.fromI32(1)], 'Les tableaux doivent être égaux')assert.tupleEquals(changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),'Les tuples doivent être égaux',)assert.assertTrue(true, 'Doit être vrai')assert.assertNull(null, 'Doit être nul')assert.assertNotNull('pas nul', 'Ne doit pas être nul')assert.entityCount('Gravatar', 1, 'Il devrait y avoir 2 gravatars')assert.dataSourceCount('GraphTokenLockWallet', 1, 'Le modèle(template) GraphTokenLockWallet doit avoir une source de données')assert.dataSourceExists('GraphTokenLockWallet',Address.zero().toHexString(),'GraphTokenLockWallet doit avoir une source de données pour l'adresse zéro',)
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()
À partir de la version 0.6.0, logStore
n'affiche plus les champs dérivés, les utilisateurs peuvent utiliser la nouvelle fonction logEntity
. Bien sûr, logEntity
peut être utilisé pour afficher n'importe quelle entité, pas seulement celles qui ont des champs dérivés. logEntity
prend le type d'entité, l'Id de l'entité et un paramètre showRelated
pour indiquer si les utilisateurs veulent afficher les entités dérivées liées.
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.
Tester les champs dérivés est une fonctionnalité qui permet aux utilisateurs de définir un champ sur une certaine entité et de faire en sorte qu'une autre entité soit automatiquement mise à jour si elle dérive l'un de ses champs de la première entité.
Avant la version 0.6.0
, il était possible d'obtenir les entités dérivées en les accédant comme des champs/propriétés d'entité, comme ceci :
let entity = ExampleEntity.load('id')let derivedEntity = entity.derived_entity
À partir de la version 0.6.0
, cela se fait en utilisant la fonction loadRelated
de graph-node, les entités dérivées peuvent être accessibles de la même manière que dans les gestionnaires.
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)})
Depuis la version 0.6.0
, les utilisateurs peuvent tester loadInBlock
en utilisant mockInBlockStore
, ce qui permet de simuler des entités dans le cache du bloc.
import { afterAll, beforeAll, describe, mockInBlockStore, test } from 'matchstick-as'import { Gravatar } from '../../generated/schema'describe('loadInBlock', () => {beforeAll(() => {mockInBlockStore('Gravatar', 'gravatarId0', gravatar)})afterAll(() => {clearInBlockStore()})test('On peut utiliser entity.loadInBlock() pour récupérer l'entité dans le cache du bloc actuel', () => {let retrievedGravatar = Gravatar.loadInBlock('gravatarId0')assert.stringEquals('gravatarId0', retrievedGravatar!.get('id')!.toString())})test("Renvoie null lors de l'appel de entity.loadInBlock() si une entité n'existe pas dans le bloc actuel", () => {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.
Depuis la version 0.6.0
, il est possible de tester si une nouvelle source de données a été créée à partir d'un modèle. Cette fonctionnalité prend en charge les modèles ethereum/contrat et file/ipfs. Il existe quatre fonctions pour cela :
assert.dataSourceCount(templateName, expectedCount)
peut être utilisée pour affirmer le nombre attendu de sources de données à partir du modèle spécifiéassert.dataSourceExists(templateName, address/ipfsHash)
affirme qu'une source de données avec l'identifiant spécifié (qui peut être une adresse de contrat ou un hash de fichier IPFS) a été créée à partir d'un modèle spécifiélogDataSources(templateName)
affiche toutes les sources de données à partir du modèle spécifié dans la console à des fins de débogagereadFile(path)
lit un fichier JSON qui représente un fichier IPFS et retourne le contenu sous forme de Bytes
test('ethereum/contract dataSource creation example', () => {// affirme qu'aucune source de données n'est créée à partir du modèle GraphTokenLockWalletassert.dataSourceCount('GraphTokenLockWallet', 0)// Crée une nouvelle source de données GraphTokenLockWallet avec l'adresse 0xA16081F360e3847006dB660bae1c6d1b2e17eC2AGraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))// affirme que la source de données a été crééeassert.dataSourceCount('GraphTokenLockWallet', 1)// Ajoute une seconde source de données avec contextelet context = new DataSourceContext()context.set('contextVal', Value.fromI32(325))GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)// Vérifie qu'il y a maintenant 2 sources de donnéesassert.dataSourceCount('GraphTokenLockWallet', 2)// affirme qu'une source de données avec l'adresse "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B" a été créée// Gardez à l'esprit que le type `Address` est transformé en minuscules lors du décodage, vous devez donc passer l'adresse en minuscules lorsque vous affirmez son existenceassert.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}}}
De même que les sources de données dynamiques de contrat, les utilisateurs peuvent tester les fichiers sources de données test et leurs gestionnaires
---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
"""Portefeuilles de verrouillage de jetons qui contiennent des GRT verrouillés"""type TokenLockMetadata @entity {"L'adresse du portefeuille de blocage des jetons"id: ID!"Heure de début du calendrier de sortie"startTime: BigInt!"Heure de fin du calendrier de sortie""endTime: BigInt!"Nombre de périodes entre l'heure de début et l'heure de fin"periods: BigInt!"Heure à laquelle commence la sortie"releaseStartTime: BigInt!}
{"startTime": 1,"endTime": 1,"periods": 1,"releaseStartTime": 1}
export function handleMetadata(content: Bytes): void {// dataSource.stringParams() renvoie le CID du fichier de source de donnée// stringParam() sera simulé dans le test du gestionnaire// pour plus d'informations 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('exemple de création de source de données file/ipfs', () => {// Générer le CID de la source de données à partir du ipfsHash + chemin du fichier du ipfs// Par exemple QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.jsonconst ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'const CID = `${ipfshash}/example.json`// Création d'une nouvelle source de données en utilisant le CID généréGraphTokenLockMetadata.create(CID)// Affirmer que la source de données a été crééeassert.dataSourceCount('GraphTokenLockMetadata', 1)assert.dataSourceExists('GraphTokenLockMetadata', CID)logDataSources('GraphTokenLockMetadata')// Maintenant, nous devons simuler les métadonnées de la source de données et plus particulièrement dataSource.stringParam()// dataSource.stringParams utilise en fait la valeur de dataSource.address(), donc nous allons simuler l'adresse en utilisant dataSourceMock de matchstick-as// Tout d'abord, nous allons réinitialiser les valeurs et ensuite utiliser dataSourceMock.setAddress() pour définir le CIDdataSourceMock.resetValues()dataSourceMock.setAddress(CID)// Maintenant, nous devons générer les Bytes à passer au gestionnaire de la source de données// Pour ce cas, nous avons introduit une nouvelle fonction readFile, qui lit un json local et renvoie le contenu sous forme de Bytesconst content = readFile('path/to/metadata.json')handleMetadata(content)// Maintenant, nous allons tester si un TokenLockMetadata a été créé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))})
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
Ceci 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.
For any additional support, check out this .
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.