subgraphs > Developing > Creating > Фреймворк модульного тестирования

Фреймворк модульного тестирования

Reading time: 24 min

Learn how to use Matchstick, a unit testing framework developed by LimeChain. Matchstick enables subgraph developers to test their mapping logic in a sandboxed environment and sucessfully deploy their subgraphs.

Benefits of Using Matchstick

Ссылка на этот раздел
  • 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

Создайте символическую ссылку на последнюю версию libpq.5.lib Возможно, сначала Вам потребуется создать этот каталог /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 install postgresql

Using WSL (Windows Subsystem for Linux)

Ссылка на этот раздел

Вы можете использовать Matchstick в WSL как с помощью подхода Docker, так и с помощью бинарного подхода. Поскольку WSL может быть немного сложной задачей, вот несколько советов на случай, если Вы столкнетесь с такими проблемами, как

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

или

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

Пожалуйста, убедитесь, что используете более новую версию Node.js. graph-cli больше не поддерживает v10.19.0 и по-прежнему является версией по умолчанию для новых образов Ubuntu на WSL. Например, подтверждено, что Matchstick работает на WALL с v18.1.0. Вы можете переключиться на него либо через nvm, либо, если обновите свой глобальный Node.js. Не забудьте удалить node_modules и повторно запустить npm install после обновления nodejs! Затем убедитесь, что у Вас установлена libpq. Это можно сделать, запустив

sudo apt-get install libpq-dev

И, наконец, не применяйте graph test (который использует Вашу глобальную установку graph-cli и по какой-то причине в настоящее время выглядит так, как будто он не работает в WSL). Вместо этого примените yarn test или npm run test (который будет использовать локальный экземпляр graph-cli на уровне проекта, который работает отлично). Для этого Вам, конечно, понадобится скрипт "test" в файле package.json, который может быть довольно простым, например

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

Чтобы использовать Matchstick в своём проекте subgraph, просто откройте терминал, перейдите в корневую папку своего проекта и запустите graph test [options] <datasource> - он загрузит последний двоичный файл Matchstick и запустит указанный тест или все тесты в тестовой папке (или все существующие тесты, если флаг источника данных не указан).

Это запустит все тесты в тестовой папке:

graph test

Это запустит тест с именем gravity.test.ts и/или все тесты внутри папки с именем gravity:

graph test gravity

Это запустит только конкретный тестовый файл:

graph test path/to/file.test.ts

Параметры:

-c, --coverage Запускает тесты в режиме покрытия
-d, --docker Запускает тесты в docker-контейнере (Примечание: пожалуйста, выполняйте из корневой папки субграфа)
-f, --force Binary: повторно загружает двоичный файл. Docker: Повторно загружает файл Docker и перестраивает образ docker
-h, --help Показывает информацию об использовании
-l, --logs Выводит на консоль информацию об операционной системе, модели процессора и URL-адресе загрузки (в целях отладки)
-r, --recompile Принудительно перекомпилирует тесты
-v, --version <tag> Выберите версию бинарного файла rust, которую хотите загрузить/использовать

Из graph-cli 0.25.2 команда graph test поддерживает запуск matchstick в контейнере docker с флагом -d. Реализация docker использует [bind mount](https://docs.docker.com/storage/bind-mounts /), чтобы не приходилось перестраивать образ docker каждый раз, когда выполняется команда graph test -d. В качестве альтернативы Вы можете следовать инструкциям из репозитория matchstick для запуска docker вручную.

❗ Команда graph test -d принудительно запускает docker run с флагом -t. Этот флаг необходимо удалить для запуска в неинтерактивных средах (таких, например, как GitHub CI).

❗ Если Вы ранее запускали graph test, Вы можете столкнуться со следующей ошибкой во время сборки docker:

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

В этом случае создайте в корневой папке .dockerignore и добавьте node_modules/binary-install-raw/bin

Конфигурация

Ссылка на этот раздел

Matchstick можно настроить на использование пользовательских тестов, библиотек и пути к манифесту через файл конфигурации matchstick.yaml:

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

Демонстрационный субграф

Ссылка на этот раздел

Вы можете попробовать и поиграть с примерами из этого руководства, клонировав Демонстрационный репозиторий субграфов

Также Вы можете посмотреть серию видеороликов >"Как использовать Matchstick для написания модульных тестов для Ваших субграфов"

Структура тестов

Ссылка на этот раздел

ВАЖНО: Описанная ниже тестовая структура зависит от версии matchstick-as >=0.5.0

describe(name: String , () => {}) - Определяет тестовую группу.

Примечания:

  • Описания не являются обязательными. Вы по-прежнему можете использовать test() как и раньше, вне блоков describe()

Пример:

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

Пример вложенной функции describe():

import { describe, test } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar } from "../../src/gravity"
describe("handleUpdatedGravatar()", () => {
describe("When entity exists", () => {
test("updates the entity", () => {
...
})
})
describe("When entity does not exists", () => {
test("it creates a new entity", () => {
...
})
})
})

test(name: String, () =>, should_fail: bool) - Определяет тестовый пример. Вы можете использовать test() внутри блоков describe() или независимо друг от друга.

Пример:

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

или

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

Запускает блок кода перед любым из тестов в файле. Если beforeAll объявлен внутри блока describe, он запускается в начале этого блока describe.

Примеры:

Код внутри beforeAll будет выполнен один раз перед всеми тестами в файле.

import { describe, test, beforeAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { Gravatar } from "../../generated/schema"
beforeAll(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = “First Gravatar”
gravatar.save()
...
})
describe("When the entity does not exist", () => {
test("it should create a new Gravatar with id 0x1", () => {
...
})
})
describe("When entity already exists", () => {
test("it should update the Gravatar with id 0x0", () => {
...
})
})

Код внутри beforeAll будет выполняться один раз перед всеми тестами в первом блоке описания

import { describe, test, beforeAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { Gravatar } from "../../generated/schema"
describe("handleUpdatedGravatar()", () => {
beforeAll(() => {
let gravatar = new Gravatar("0x0")
gravatar.displayName = “First Gravatar”
gravatar.save()
...
})
test("updates Gravatar with id 0x0", () => {
...
})
test("creates new Gravatar with id 0x1", () => {
...
})
})

Запускает блок кода после выполнения всех тестов в файле. Если afterAll объявлен внутри блока describe, он запускается в конце этого блока describe.

Пример:

Код внутри afterAll будет выполнен один раз после всех тестов в файле.

import { describe, test, afterAll } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
import { store } from "@graphprotocol/graph-ts"
afterAll(() => {
store.remove("Gravatar", "0x0")
...
})
describe("handleNewGravatar, () => {
test("creates Gravatar with id 0x0", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("updates Gravatar with id 0x0", () => {
...
})
})

Код внутри afterAll будет выполнен один раз после всех тестов в первом блоке описания

import { describe, test, afterAll, clearStore } from "matchstick-as/assembly/index"
import { handleUpdatedGravatar, handleNewGravatar } from "../../src/gravity"
describe("handleNewGravatar", () => {
afterAll(() => {
store.remove("Gravatar", "0x1")
...
})
test("It creates a new entity with Id 0x0", () => {
...
})
test("It creates a new entity with Id 0x1", () => {
...
})
})
describe("handleUpdatedGravatar", () => {
test("updates Gravatar with id 0x0", () => {
...
})
})

Запускает блок кода перед каждым тестом. Если beforeEach объявлен внутри блока describe, он запускается перед каждым тестом в этом блоке describe.

Примеры: Код внутри beforeEach будет выполняться перед каждым тестированием.

import { describe, test, beforeEach, clearStore } from "matchstick-as/assembly/index"
import { handleNewGravatars } from "./utils"
beforeEach(() => {
clearStore() // <-- clear the store before each test in the file
})
describe("handleNewGravatars, () => {
test("A test that requires a clean store", () => {
...
})
test("Second that requires a clean store", () => {
...
})
})
...

Код внутри beforeEach будет выполняться только перед каждым тестом в описании

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')
// код, который должен обновить displayName до 1-го Gravatar
assert.fieldEquals('Gravatar', '0x0', 'displayName', '1st Gravatar')
store.remove('Gravatar', '0x0')
})
test('Updates the imageUrl', () => {
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', '')
// код, который должен изменить imageUrl на https://www.gravatar.com/avatar/0x0
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', 'https://www.gravatar.com/avatar/0x0')
store.remove('Gravatar', '0x0')
})
})

Запускает блок кода после каждого теста. Если afterEach объявлен внутри блока describe, он запускается после каждого теста в этом блоке describe.

Примеры:

Код внутри afterEach будет выполняться после каждого теста.

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")
// код, который должен обновить displayName до 1-го Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// код, который должен изменить imageUrl на https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})

Код внутри afterEach будет выполняться после каждого теста в этом описании

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")
// код, который должен обновить displayName до 1-го Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// код, который должен изменить imageUrl на https://www.gravatar.com/avatar/0x0
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")
})
})
fieldEquals(entityType: string, id: string, fieldName: string, expectedVal: string)
equals(expected: ethereum.Value, actual: ethereum.Value)
notInStore(entityType: string, id: string)
addressEquals(address1: Address, address2: Address)
bytesEquals(bytes1: Bytes, bytes2: Bytes)
i32Equals(number1: i32, number2: i32)
bigIntEquals(bigInt1: BigInt, bigInt2: BigInt)
booleanEquals(bool1: boolean, bool2: boolean)
stringEquals(string1: string, string2: string)
arrayEquals(array1: Array<ethereum.Value>, array2: Array<ethereum.Value>)
tupleEquals(tuple1: ethereum.Tuple, tuple2: ethereum.Tuple)
assertTrue(value: boolean)
assertNull<T>(value: T)
assertNotNull<T>(value: T)
entityCount(entityType: string, expectedCount: i32)

Начиная с версии 0.6.0, утверждения также поддерживают настраиваемые сообщения об ошибках

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

Напишите юнит-тест

Ссылка на этот раздел

Давайте посмотрим, как будет выглядеть простой юнит-тест, используя примеры Gravatar в Демонстрационном субграфе.

Предположим, у нас есть следующая функция-обработчик (наряду с двумя вспомогательными функциями, облегчающими нашу жизнь):

export function handleNewGravatar(event: NewGravatar): void {
let gravatar = new Gravatar(event.params.id.toHex())
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}
export function handleNewGravatars(events: NewGravatar[]): void {
events.forEach((event) => {
handleNewGravatar(event)
})
}
export function createNewGravatarEvent(
id: i32,
ownerAddress: string,
displayName: string,
imageUrl: string,
): NewGravatar {
let mockEvent = newMockEvent()
let newGravatarEvent = new NewGravatar(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
mockEvent.parameters,
)
newGravatarEvent.parameters = new Array()
let idParam = new ethereum.EventParam('id', ethereum.Value.fromI32(id))
let addressParam = new ethereum.EventParam(
'ownderAddress',
ethereum.Value.fromAddress(Address.fromString(ownerAddress)),
)
let displayNameParam = new ethereum.EventParam('displayName', ethereum.Value.fromString(displayName))
let imageUrlParam = new ethereum.EventParam('imageUrl', ethereum.Value.fromString(imageUrl))
newGravatarEvent.parameters.push(idParam)
newGravatarEvent.parameters.push(addressParam)
newGravatarEvent.parameters.push(displayNameParam)
newGravatarEvent.parameters.push(imageUrlParam)
return newGravatarEvent
}

Сначала мы должны создать тестовый файл в нашем проекте. Вот пример того, как это могло бы выглядеть:

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', () => {
// Создайте тестовый объект и сохраните его в хранилище как исходное состояние (необязательно)
let gravatar = new Gravatar('gravatarId0')
gravatar.save()
// Создайте фиктивные события
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
// Вызовите функции мэппинга, передающие события, которые мы только что создали
handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
// Подтвердите состояние хранилища
assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')
// Очистите хранилище, чтобы начать следующий тест с чистого листа
clearStore()
})
test('Next test', () => {
//...
})

Предстоит очень многое распаковать! Прежде всего, важно отметить, что мы импортируем данные из matchstick-as, нашей вспомогательной библиотеки AssemblyScript (распространяемой как модуль npm). Репозиторий Вы можете найти здесь. matchstick-as предоставляет нам полезные методы тестирования, а также определяет функцию test(), которую мы будем использовать для построения наших тестовых блоков. В остальном все довольно просто - вот что происходит:

  • Мы настраиваем наше исходное состояние и добавляем один пользовательский объект Gravatar;
  • Мы определяем два объекта события NewGravatar вместе с их данными, используя функцию create New Gravatar Event();
  • Мы вызываем методы-обработчики этих событий - обрабатываем новые Gravatars() и передаем список наших пользовательских событий;
  • Мы утверждаем состояние хранилища. Как это происходит? - Мы передаем уникальную комбинацию типа объекта и идентификатора. Затем мы проверяем конкретное поле в этом объекте и утверждаем, что оно имеет то значение, которое мы ожидаем от него получить. Мы делаем это как для исходного объекта Gravatar, который мы добавили в хранилище, так и для двух объектов Gravatar, которые добавляются при вызове функции-обработчика;
  • И, наконец, мы очищаем хранилище с помощью clear Store(), чтобы наш следующий тест можно было начать с нового и пустого объекта хранилища. Мы можем определить столько тестовых блоков, сколько захотим.

Вот и все - мы создали наш первый тест! 👏

Теперь, чтобы запустить наши тесты, Вам просто нужно запустить в корневой папке своего субграфа следующее:

graph test Gravity

И если все пойдет хорошо, Вы увидите следующее приветствие:

Matchstick с надписью “Все тесты пройдены!”

Распространенные сценарии тестирования

Ссылка на этот раздел

Наполнение хранилища до определенного состояния

Ссылка на этот раздел

Пользователи могут наполнять хранилище известным набором объектов. Вот пример инициализации хранилища с помощью объекта Gravatar:

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

Вызов функции мэппинга с помощью события

Ссылка на этот раздел

Пользователь может создать пользовательское событие и передать его функции мэппинга, привязанной к хранилищу:

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)

Вызов всех мэппингов с фиксированными событиями

Ссылка на этот раздел

Пользователи могут вызывать мэппинги с помощью тестовых наборов данных.

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

Имитация вызовов контракта

Ссылка на этот раздел

Пользователи могут имитировать вызовы контракта:

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

Как было продемонстрировано, для того, чтобы имитировать вызов контракта и хардкор возвращаемого значения, пользователь должен предоставить адрес контракта, имя функции, сигнатуру функции, массив аргументов и, конечно же, возвращаемое значение.

Пользователи также могут имитировать возврат функций:

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

Имитация файлов IPFS (из matchstick 0.4.1)

Ссылка на этот раздел

Пользователи могут имитировать файлы IPFS с помощью функции mockIpfsFile(hash, filePath). Функция принимает два аргумента, первый из которых - хэш/путь к файлу IPFS, а второй - путь к локальному файлу.

ПРИМЕЧАНИЕ: При тестировании ipfs.map/ipfs.mapJSON функция обратного вызова должна быть экспортирована из тестового файла, чтобы matchstck мог ее обнаружить, подобно функции processGravatar() в приведенном ниже примере теста:

Файл .test.ts:

import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'
import { ipfs } from '@graphprotocol/graph-ts'
import { gravatarFromIpfs } from './utils'
// Экспортируйте обратный вызов ipfs.map(), чтобы matchstick мог его обнаружить
export { processGravatar } from './utils'
test('ipfs.cat', () => {
mockIpfsFile('ipfsCatfileHash', 'tests/ipfs/cat.json')
assert.entityCount(GRAVATAR_ENTITY_TYPE, 0)
gravatarFromIpfs()
assert.entityCount(GRAVATAR_ENTITY_TYPE, 1)
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '1', 'imageUrl', 'https://i.ytimg.com/vi/MELP46s8Cic/maxresdefault.jpg')
clearStore()
})
test('ipfs.map', () => {
mockIpfsFile('ipfsMapfileHash', 'tests/ipfs/map.json')
assert.entityCount(GRAVATAR_ENTITY_TYPE, 0)
ipfs.map('ipfsMapfileHash', 'processGravatar', Value.fromString('Gravatar'), ['json'])
assert.entityCount(GRAVATAR_ENTITY_TYPE, 3)
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '1', 'displayName', 'Gravatar1')
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '2', 'displayName', 'Gravatar2')
assert.fieldEquals(GRAVATAR_ENTITY_TYPE, '3', 'displayName', 'Gravatar3')
})

Файл utils.ts:

import { Address, ethereum, JSONValue, Value, ipfs, json, Bytes } from "@graphprotocol/graph-ts"
import { Gravatar } from "../../generated/schema"
...
// обратный вызов ipfs.map
export function processGravatar(value: JSONValue, userData: Value): void {
// Смотрите документацию по JsonValue для получения подробной информации о работе
// со значениями JSON
let obj = value.toObject()
let id = obj.get('id')
if (!id) {
return
}
// Обратные вызовы также могут создавать объекты
let gravatar = new Gravatar(id.toString())
gravatar.displayName = userData.toString() + id.toString()
gravatar.save()
}
// функция, которая вызывает ipfs.cat
export function gravatarFromIpfs(): void {
let rawData = ipfs.cat("ipfsCatfileHash")
if (!rawData) {
return
}
let jsonData = json.fromBytes(rawData as Bytes).toObject()
let id = jsonData.get('id')
let url = jsonData.get("imageUrl")
if (!id || !url) {
return
}
let gravatar = new Gravatar(id.toString())
gravatar.imageUrl = url.toString()
gravatar.save()
}

Подтверждение состояния хранилища

Ссылка на этот раздел

Пользователи могут утверждать конечное (или промежуточное) состояние хранилища с помощью утверждающих объектов. Для этого пользователь должен указать тип объекта, конкретный идентификатор объекта, имя поля этого объекта и ожидаемое значение поля. Вот небольшой пример:

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

Запуск функции assert.field Equals() проверит соответствие данного поля заданному ожидаемому значению. Тест завершится неудачей, и будет выведено сообщение об ошибке, если значения НЕ равны. В противном случае тест пройдет успешно.

Взаимодействие с метаданными событий

Ссылка на этот раздел

Пользователи могут по умолчанию использовать метаданные транзакции, которые могут быть возвращены в виде ethereum.Event с помощью функции new MockEvent(). В следующем примере показано, как можно считывать/записывать данные в эти поля объекта Event:

// Чтение
let logType = newGravatarEvent.logType
// Запись
let UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'
newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)

Утверждение равенства переменных

Ссылка на этот раздел
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

Утверждение о том, что объект отсутствует в хранилище

Ссылка на этот раздел

Пользователи могут утверждать, что объект отсутствует в хранилище. Функция принимает тип объекта и идентификатор. Если объект действительно находится в хранилище, тест завершится неудачей с соответствующим сообщением об ошибке. Вот краткий пример использования этой функции:

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

Вывод всего хранилища или отдельных его объектов (в целях отладки)

Ссылка на этот раздел

С помощью этой вспомогательной функции можно вывести всё хранилище на консоль:

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

Начиная с версии 0.6.0, logStore больше не выводит производные поля. Вместо этого пользователи могут использовать новую функцию logEntity. Конечно, logEntity можно использовать для вывода любого объекта, а не только тех, у которых есть производные поля. logEntity принимает тип объекта, идентификатор объекта и флаг showRelated, указывающий, хочет ли пользователь вывести связанные производные объекты.

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

Ожидаемый сбой

Ссылка на этот раздел

Пользователи могут ожидать сбоев тестирования, используя флаг shouldFail в функциях test():

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

Если тест помечен как shouldFail = true, но НЕ завершается неудачей, это отобразится как ошибка в логах, и тестовый блок завершится неудачей. Также, если он помечен как shouldFail = false (состояние по умолчанию), произойдет сбой тестового исполнителя.

Логирование (ведение журналов)

Ссылка на этот раздел

Наличие пользовательских логов в модульных тестах - это точно то же самое, что логирование в мэппингах. Разница заключается в том, что объект лога необходимо импортировать из matchstick-as, а не из graph-ts. Вот простой пример со всеми некритическими типами логов:

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

Пользователи также могут имитировать критический сбой, например, так:

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

Логирование критических ошибок остановит выполнение тестов и все испортит. В конце концов, мы хотим быть уверены, что Ваш код не содержит критических логов при развертывании, и Вы сразу заметите, если это произойдет.

Тестирование производных полей

Ссылка на этот раздел

Тестирование производных полей — это функция, которая позволяет пользователям устанавливать поле для определенного объекта и автоматически обновлять другой объект, если он извлекает одно из своих полей из первого объекта.

До версии 0.6.0 можно было получить производные объекты, обратившись к ним как к полям/свойствам объектов, например:

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

Начиная с версии 0.6.0 это делается с помощью функции loadRelated graph-node, к производным объектам можно получить доступ так же, как и в обработчиках.

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

Тестирование loadInBlock

Ссылка на этот раздел

Начиная с версии 0.6.0, пользователи могут тестировать loadInBlock с помощью mockInBlockStore, он позволяет имитировать объекты в кеше блоков.

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

Тестирование динамических источников данных

Ссылка на этот раздел

Тестирование динамических источников данных может быть выполнено путем имитации возвращаемого значения функций context(), address() и network() пространства имен dataSource. В настоящее время эти функции возвращают следующее: context() - возвращает пустой объект (DataSourceContext), address() - возвращает 0x0000000000000000000000000000000000000000 network() - возвращает mainnet. Функции create(...) и createWithContext(...) замаскированы так, что они не выполняют никаких действий, поэтому их вообще не нужно вызывать в тестах. Изменения возвращаемых значений могут быть выполнены с помощью функций пространства имен dataSourceMock в matchstick-as (версия 0.3.0+).

Пример ниже:

Во-первых, у нас есть следующий обработчик событий (который был намеренно перепрофилирован для демонстрации искусственного искажения источника данных):

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

Во-вторых, у нас есть тест, использующий один из методов в пространстве имён dataSourceMock для установки нового возвращаемого значения для всех функций dataSource:

import { assert, test, newMockEvent, dataSourceMock } from 'matchstick-as/assembly/index'
import { BigInt, DataSourceContext, Value } from '@graphprotocol/graph-ts'
import { handleApproveTokenDestinations } from '../../src/token-lock-wallet'
import { ApproveTokenDestinations } from '../../generated/templates/GraphTokenLockWallet/GraphTokenLockWallet'
import { TokenLockWallet } from '../../generated/schema'
test('Data source simple mocking example', () => {
let addressString = '0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'
let address = Address.fromString(addressString)
let wallet = new TokenLockWallet(address.toHexString())
wallet.save()
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
dataSourceMock.setReturnValues(addressString, 'rinkeby', context)
let event = changetype<ApproveTokenDestinations>(newMockEvent())
assert.assertTrue(!wallet.tokenDestinationsApproved)
handleApproveTokenDestinations(event)
wallet = TokenLockWallet.load(address.toHexString())!
assert.assertTrue(wallet.tokenDestinationsApproved)
assert.bigIntEquals(wallet.tokensReleased, BigInt.fromI32(325))
dataSourceMock.resetValues()
})

Обратите внимание, что функция DataSource Mock.resetValues() вызывается в конце. Это происходит потому, что значения запоминаются при их изменении, и их необходимо сбросить, если Вы хотите вернуться к значениям по умолчанию.

Тестирование создания источника динамических данных

Ссылка на этот раздел

Начиная с версии 0.6.0, можно проверить, был ли создан новый источник данных из шаблона. Эта функция поддерживает шаблоны ethereum/contract и file/ipfs. Для этого предусмотрены четыре функции:

  • assert.dataSourceCount(templateName,expectedCount) можно использовать для подтверждения ожидаемого количества источников данных из указанного шаблона
  • assert.dataSourceExists(templateName, адрес/ipfsHash) подтверждает, что источник данных с указанным идентификатором (может быть адресом контракта или хешем файла IPFS) из указанного шаблона был создан
  • logDataSources(templateName) выводит все источники данных из указанного шаблона на консоль в целях отладки
  • readFile(path) считывает файл JSON, представляющий файл IPFS, и возвращает содержимое в виде байтов

Тестирование шаблонов ethereum/contract

Ссылка на этот раздел
test('ethereum/contract dataSource creation example', () => {
// Подтверждаем, что не создано ни одного источника данных из шаблона GraphTokenLockWallet
assert.dataSourceCount('GraphTokenLockWallet', 0)
// Создаем новый источник данных GraphTokenLockWallet с адресом 0xA16081F360e3847006dB660bae1c6d1b2e17eC2A
GraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))
// Подтверждаем, что источник данных создан
assert.dataSourceCount('GraphTokenLockWallet', 1)
// Добавляем второй источник данных с контекстом
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)
// Подтверждаем, что теперь есть 2 источника данных
assert.dataSourceCount('GraphTokenLockWallet', 2)
// Подтверждаем, что был создан источник данных с адресом "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B"
// Имейте в виду, что тип `Address` преобразуется в строчные буквы при декодировании, поэтому адрес нужно передавать полностью в нижнем регистре, когда Вы проверяете его наличие.
assert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())
logDataSources('GraphTokenLockWallet')
})
Пример вывода logDataSource
Ссылка на этот раздел
🛠 {
"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
"context": null
},
"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b",
"context": {
"contextVal": {
"type": "Int",
"data": 325
}
}
}
}

Тестирование шаблонов file/ipfs

Ссылка на этот раздел

Аналогично контрактным источникам динамических данных пользователи могут тестировать источники данных тестовых файлов и их обработчики

Пример subgraph.yaml
Ссылка на этот раздел
...
templates:
- kind: file/ipfs
name: GraphTokenLockMetadata
network: mainnet
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/token-lock-wallet.ts
handler: handleMetadata
entities:
- TokenLockMetadata
abis:
- name: GraphTokenLockWallet
file: ./abis/GraphTokenLockWallet.json
Пример schema.graphql
Ссылка на этот раздел
"""
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!
}
Пример metadata.json
Ссылка на этот раздел
{
"startTime": 1,
"endTime": 1,
"periods": 1,
"releaseStartTime": 1
}
Пример обработчика
Ссылка на этот раздел
export function handleMetadata(content: Bytes): void {
// dataSource.stringParams() возвращает CID источника данных файла
// stringParam() будет имитироваться в тесте обработчика
// для получения дополнительной информации https://thegraph.com/docs/en/developing/creating-a-subgraph/#create-a-new-handler-to-process-files
let tokenMetadata = new TokenLockMetadata(dataSource.stringParam())
const value = json.fromBytes(content).toObject()
if (value) {
const startTime = value.get('startTime')
const endTime = value.get('endTime')
const periods = value.get('periods')
const releaseStartTime = value.get('releaseStartTime')
if (startTime && endTime && periods && releaseStartTime) {
tokenMetadata.startTime = startTime.toBigInt()
tokenMetadata.endTime = endTime.toBigInt()
tokenMetadata.periods = periods.toBigInt()
tokenMetadata.releaseStartTime = releaseStartTime.toBigInt()
}
tokenMetadata.save()
}
}
import { assert, test, dataSourceMock, readFile } from 'matchstick-as'
import { Address, BigInt, Bytes, DataSourceContext, ipfs, json, store, Value } from '@graphprotocol/graph-ts'
import { handleMetadata } from '../../src/token-lock-wallet'
import { TokenLockMetadata } from '../../generated/schema'
import { GraphTokenLockMetadata } from '../../generated/templates'
test('file/ipfs dataSource creation example', () => {
// Сгенерируйте the dataSource CID from the ipfsHash + ipfs path file
// Например, QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.json
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
const CID = `${ipfshash}/example.json`
// Создайте новый источник данных, используя сгенерированный CID
GraphTokenLockMetadata.create(CID)
// Подтвердите, что источник данных создан
assert.dataSourceCount('GraphTokenLockMetadata', 1)
assert.dataSourceExists('GraphTokenLockMetadata', CID)
logDataSources('GraphTokenLockMetadata')
// Теперь нам нужно смоделировать метаданные dataSource, в частности,
dataSource.stringParam()
// dataSource.stringParams на самом деле использует значение dataSource.address(), поэтому мы будем имитировать адрес, используя dataSourceMock из matchstick-as
// Сначала мы сбросим значения, а затем используем dataSourceMock.setAddress() для установки CID
dataSourceMock.resetValues()
dataSourceMock.setAddress(CID)
// Теперь нам нужно сгенерировать байты для передачи обработчику dataSource
// Для этого случая мы ввели новую функцию readFile, которая считывает локальный json и возвращает содержимое в виде байтов
const content = readFile(`path/to/metadata.json`)
handleMetadata(content)
// Теперь проверим, был ли создан TokenLockMetadata
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))
})

Тестовое покрытие

Ссылка на этот раздел

Используя Matchstick, разработчики субграфов могут запустить скрипт, который вычислит тестовое покрытие написанных модульных тестов.

Инструмент тестового покрытия берет скомпилированные тестовые двоичные файлы wasm и преобразует их в файлы wat, которые затем можно легко проверить, были ли вызваны обработчики, определенные в subgraph.yaml. Поскольку покрытие кода (и тестирование в целом) в AssemblyScript и WebAssembly находится на очень ранних стадиях, Matchstick не может проверить покрытие ветвей. Вместо этого мы полагаемся на утверждение, что если был вызван данный обработчик, то событие/функция для него были должным образом имитированы.

Предварительные требования

Ссылка на этот раздел

Чтобы запустить функцию тестового покрытия, представленную в Matchstick, необходимо заранее подготовить несколько вещей:

Экспортируйте свои обработчики

Ссылка на этот раздел

Для того чтобы Matchstick мог проверить, какие обработчики запущены, эти обработчики необходимо экспортировать из тестового файла. Так, например, в файле gravity.test.ts импортируется следующий обработчик:

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

Чтобы эта функция была видимой (чтобы она была включена в файл wat под именем), нам нужно также экспортировать ее, например, так:

export { handleNewGravatar }

После того как всё это будет настроено, чтобы запустить инструмент тестового покрытия, просто запустите:

graph test -- -c

Вы также можете добавить пользовательскую команду coverage в свой файл package.json, например, так:

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

При этом запустится инструмент покрытия, и в терминале Вы должны увидеть что-то вроде этого:

$ graph test -c
Skipping download/install step because binary already exists at /Users/petko/work/demo-subgraph/node_modules/binary-install-raw/bin/0.4.0
___ ___ _ _ _ _ _
| \/ | | | | | | | (_) | |
| . . | __ _| |_ ___| |__ ___| |_ _ ___| | __
| |\/| |/ _` | __/ __| '_ \/ __| __| |/ __| |/ /
| | | | (_| | || (__| | | \__ \ |_| | (__| <
\_| |_/\__,_|\__\___|_| |_|___/\__|_|\___|_|\_\
Compiling...
Running in coverage report mode.
Reading generated test modules... 🔎️
Generating coverage report 📝
Handlers for source 'Gravity':
Handler 'handleNewGravatar' is tested.
Handler 'handleUpdatedGravatar' is not tested.
Handler 'handleCreateGravatar' is tested.
Test coverage: 66.7% (2/3 handlers).
Handlers for source 'GraphTokenLockWallet':
Handler 'handleTokensReleased' is not tested.
Handler 'handleTokensWithdrawn' is not tested.
Handler 'handleTokensRevoked' is not tested.
Handler 'handleManagerUpdated' is not tested.
Handler 'handleApproveTokenDestinations' is not tested.
Handler 'handleRevokeTokenDestinations' is not tested.
Test coverage: 0.0% (0/6 handlers).
Global test coverage: 22.2% (2/9 handlers).

Продолжительность выполнения теста в выходных данных лога

Ссылка на этот раздел

Выходные данные лога включают в себя продолжительность тестового запуска. Вот пример:

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

Типичные ошибки компилятора

Ссылка на этот раздел

Критично: Не удалось создать WasmInstance из допустимого модуля с контекстом: неизвестный импорт: wasi_snapshot_preview1::fd_write не определен

Это означает, что Вы использовали в своем коде console.log, который не поддерживается AssemblyScript. Пожалуйста, рассмотрите возможность использования API логирования

ERROR TS2554: Expected ? arguments, but got ?.

return new ethereum.Block(defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultAddress, defaultAddressBytes, defaultAddressBytes, defaultAddressBytes, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt, defaultBigInt);

in ~lib/matchstick-as/assembly/defaults.ts(18,12)

ERROR TS2554: Expected ? arguments, but got ?.

return new ethereum.Transaction(defaultAddressBytes, defaultBigInt, defaultAddress, defaultAddress, defaultBigInt, defaultBigInt, defaultBigInt, defaultAddressBytes, defaultBigInt);

in ~lib/matchstick-as/assembly/defaults.ts(24,12)

Несовпадение в аргументах вызвано несоответствием в graph-ts и matchstick-as. Лучший способ устранить проблемы, подобные этой, - обновить всё до последней выпущенной версии.

Дополнительные источники

Ссылка на этот раздел

For any additional support, check out this demo subgraph repo using Matchstick.

Обратная связь

Ссылка на этот раздел

Если у Вас есть какие-либо вопросы, отзывы, пожелания по функциям или Вы просто хотите связаться с нами, лучшим местом будет Graph Discord, где у нас есть выделенный канал для Matchstick под названием 🔥| unit-testing.

Редактировать страницу

Предыдущий
Распространенные проблемы с AssemblyScript
Следующий
Развертывание с использованием Subgraph Studio
Редактировать страницу