开发 > 单元测试框架

单元测试框架

Reading time: 27 min

Matchstick 是一个单元测试框架,由LimeChain开发,使子图开发者能够在沙盒环境中测试他们的映射逻辑,并放心地部署他们的子图。

开始

链到本节

安装依赖项

链到本节

为了使用测试帮助器方法并运行测试,您需要安装以下依赖项:

yarn add --dev matchstick-as

graph-node 依赖于 PostgreSQL,因此如果您还没有它,就需要安装它。我们强烈建议使用下面的命令,因为以任何其他方式添加它可能会导致意外的错误!

Postgres 安装命令:

brew install postgresql

创建到最新 libpq.5. lib* 的符号链接,可能需要首先创建这个目录*/usr/local/opt/postgreql/lib/

ln -sf /usr/local/opt/postgresql@14/lib/postgresql@14/libpq.5.dylib /usr/local/opt/postgresql/lib/libpq.5.dylib

Postgres 安装命令(取决于您的发行版):

sudo apt install postgresql

WSL(用于 Linux 的 Windows 子系统)

链到本节

可以使用 Docker 方法和二进制方法在 WSL 上使用 Matchstick。由于 WSL 可能有点复杂,所以这里有一些提示,以防您遇到诸如

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

或者

<PROJECT_PATH>>/node_modules/gluegun/build/index.js:13 抛出;

请确保您使用的是新版本的 Node.js graph-cli 不再支持 v10.19.0,而且这仍然是 WSL 上新 Ubuntu 映像的默认版本。例如,已经证实Matchstick 可以在使用 v18.1.0的 WSL 上工作,您可以通过 nvm 或者更新全局 Node.js 切换到 Matchstick。不要忘记删除 node _ module,并在更新 nodejs 之后再次运行 npm install!然后,确保已经安装了 libpq,可以通过运行

sudo apt-get install libpq-dev

最后,不要使用graph test (使用全局安装的 graph-cli,并且由于某些原因,它看起来像是在 WSL 上已经坏掉了) ,而是使用yarn testnpm run test(这将使用本地的项目级的 graph-cli 实例,它的工作原理非常有趣。)为此,您当然需要在 package.json 文件中有一个“ test”脚本,它可以简单到

{
"name": "demo-subgraph",
"version": "0.1.0",
"scripts": {
"test": "graph test",
...
},
"dependencies": {
"@graphprotocol/graph-cli": "^0.30.0",
"@graphprotocol/graph-ts": "^0.27.0",
"matchstick-as": "^0.5.0"
}
}

使用方法

链到本节

要在子图项目中使用Matchstick,只需打开一个终端,导航到项目的根文件夹,然后简单地运行graph test [options] <datasource>-它下载最新的Matchstick二进制文件,并在测试文件夹中运行指定的测试或所有测试(如果未指定数据源标志,则运行所有现有测试)。

CLI 选项

链到本节

这将运行测试文件夹中的所有测试:

graph test

这将运行名为gravity.test.ts的测试和/或名为gravity的文件夹中的所有测试:

graph test Gravity

这将仅运行特定的测试文件:

graph test path/to/file.test.ts

选项

-c, --coverage Run the tests in coverage mode
-d, --docker Run the tests in a docker container (Note: Please execute from the root folder of the subgraph)
-f, --force Binary: Redownloads the binary. Docker: Redownloads the Dockerfile and rebuilds the docker image.
-h, --help Show usage information
-l, --logs Logs to the console information about the OS, CPU model and download url (debugging purposes)
-r, --recompile Forces tests to be recompiled
-v, --version <tag> Choose the version of the rust binary that you want to be downloaded/used

Docker

链到本节

graph cli 0.25.2中,graph test命令支持在带有-d标志的docker容器中运行matchstick 。docker实现使用bind mount,因此它不必在每次执行graph test-d命令时重新构建docker映像。或者,您可以按照matchstick存储库中的说明手动运行docker。

❗如果您以前运行过graph test,则在docker构建过程中可能会遇到以下错误:

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

在本例中,在根文件夹中创建一个.dockerignore,并添加node_modules/bibinary install raw/bin

配置

链到本节

Matchstick可以通过Matchstick.yaml配置文件配置为使用自定义测试、库和清单路径:

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

演示子图

链到本节

您可以通过克隆Demo Subgraph repo来尝试并使用本指南中的示例。

视频教程

链到本节

此外,您还可以查看“如何使用Matchstick为子图编写单元测试”系列视频

测试结构(>=0.5.0)

链到本节

重要事项:需要matchstick-as >=0.5.0。

描述()

链到本节

describe(name: String , () => {}) - 定义测试组。

注意:

  • 描述不是强制性的。您仍然可以在describe()区块之外以旧的方式使用test()

例子:

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) -定义测试用例。您可以在describe()区块内部或独立使用test()。

例子:

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

链到本节

在文件中的任何测试之前运行代码区块。如果beforeAll描述区块内声明,它将在该描述区块的开头运行。

例子:

beforeAll中的代码将在文件中的all测试之前执行一次。

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

链到本节

在文件中的所有测试之后运行代码区块。如果afterAlldescribe区块内声明,它将在该describe区块的末尾运行。

例子:

afterAll中的代码将在文件中的all测试之后执行一次。

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

链到本节

在每次测试之前运行代码块。如果beforeEachdescribe区块中声明,则它在该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')
// code that should update the displayName to 1st Gravatar
assert.fieldEquals('Gravatar', '0x0', 'displayName', '1st Gravatar')
store.remove('Gravatar', '0x0')
})
test('Updates the imageUrl', () => {
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', '')
// code that should changes the imageUrl to https://www.gravatar.com/avatar/0x0
assert.fieldEquals('Gravatar', '0x0', 'imageUrl', 'https://www.gravatar.com/avatar/0x0')
store.remove('Gravatar', '0x0')
})
})

afterEach()

链到本节

在每次测试后运行代码区块。如果afterEachdescribe 区块中声明,则在该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")
// code that should update the displayName to 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// code that should changes the imageUrl to 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")
// code that should update the displayName to 1st Gravatar
assert.fieldEquals("Gravatar", "0x0", "displayName", "1st Gravatar")
})
test("Updates the imageUrl", () => {
assert.fieldEquals("Gravatar", "0x0", "imageUrl", "")
// code that should changes the imageUrl to 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)

编写一个单元测试

链到本节

让我们看看使用Demo Subgraph中的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', () => {
// Create a test entity and save it in the store as initial state (optional)
let gravatar = new Gravatar('gravatarId0')
gravatar.save()
// Create mock events
let newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')
// Call mapping functions passing the events we just created
handleNewGravatars([newGravatarEvent, anotherGravatarEvent])
// Assert the state of the store
assert.fieldEquals('Gravatar', 'gravatarId0', 'id', 'gravatarId0')
assert.fieldEquals('Gravatar', '12345', 'owner', '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7')
assert.fieldEquals('Gravatar', '3546', 'displayName', 'cap')
// Clear the store in order to start the next test off on a clean slate
clearStore()
})
test('Next test', () => {
//...
})

这太多了!首先,需要注意的一件重要事情是,我们将从matchstick-as中导入东西,作为我们的AssemblyScript助手库(作为npm模块分发)。您可以在此处找到存储库。matchstick as为我们提供了有用的测试方法,还定义了我们将用来构建测试块的test()函数。剩下的部分很简单——下面是发生的事情:

  • 我们正在设置我们的初始状态并添加一个自定义的 Gravatar 实体。
  • 我们使用createNewGravatarEvent()函数定义了两个NewGravatar事件对象以及它们的数据。
  • 我们正在为这些事件调用处理方法--handleNewGravatars(),并传入我们的自定义事件列表。
  • 我们断定存储的状态。那是怎么实现的呢?- 我们传递一个实体类型和 id 的唯一组合。然后我们检查该实体的一个特定字段,并断定它具有我们期望的值。我们为我们添加到存储的初始 Gravatar 实体,以及当处理函数被调用时被添加的两个 Gravatar 实体都做这个。
  • 最后--我们用clearStore()清理存储,这样我们的下一个测试就可以从一个新的空存储对象开始。我们可以定义任意多的测试块。

好了,我们创建了第一个测试!👏

现在,为了运行我们的测试,您只需在子图根文件夹中运行以下命令:

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文件(from matchstick 0.4.1)

链到本节

用户可以使用mockIpfsFile(hash, filePath)函数模拟IPFS文件。该函数接受两个参数,第一个是IPFS文件hash/路径,第二个是本地文件的路径。

注意:在测试ipfs.map/ipfs.mapJSON,时,必须从测试文件中导出回调函数,以便matchstck检测到它,如下面测试示例中的processGravatar() 函数:

.test.ts file:

import { assert, test, mockIpfsFile } from 'matchstick-as/assembly/index'
import { ipfs } from '@graphprotocol/graph-ts'
import { gravatarFromIpfs } from './utils'
// Export ipfs.map() callback in order for matchstck to detect it
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 file:

import { Address, ethereum, JSONValue, Value, ipfs, json, Bytes } from "@graphprotocol/graph-ts"
import { Gravatar } from "../../generated/schema"
...
// ipfs.map callback
export function processGravatar(value: JSONValue, userData: Value): void {
// See the JSONValue documentation for details on dealing
// with JSON values
let obj = value.toObject()
let id = obj.get('id')
if (!id) {
return
}
// Callbacks can also created entities
let gravatar = new Gravatar(id.toString())
gravatar.displayName = userData.toString() + id.toString()
gravatar.save()
}
// function that calls 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()
}

断定存储的状态

链到本节

用户能够通过断定实体断定存储的最终(或中途)状态。为此,用户必须提供实体类型、实体的特定ID、该实体上的字段名称以及字段的预期值。下面是一个快速示例:

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.fieldEquals()函数将检查给定字段是否与给定的预期值相等。如果值相等,测试将失败,并输出错误消息。否则,测试将成功通过。

与事件元数据交互

链到本节

用户可以使用默认的交易元数据,该元数据可以通过使用newMockEvent()函数作为ethereum.Event返回。以下示例显示了如何读取/写入Event对象上的这些字段:

// Read
let logType = newGravatarEvent.logType
// Write
let UPDATED_ADDRESS = '0xB16081F360e3847006dB660bae1c6d1b2e17eC2A'
newGravatarEvent.address = Address.fromString(UPDATED_ADDRESS)

断定变量相等

链到本节
assert.equals(ethereum.Value.fromString("hello"); ethereum.Value.fromString("hello"));

断定实体在存储中

链到本节

用户可以断定实体在存储中不存在。该函数接受实体类型和id。如果实体实际上在存储中,测试将失败,并显示相关错误消息。以下是如何使用此功能的快速示例:

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

登载整个存储(用于调试)

链到本节

您可以使用此助手功能将整个存储登载到控制台:

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

预期故障

链到本节

使用test()函数上的shouldFail标志,用户可能会出现预期的测试失败:

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

如果测试标记为should fail=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!')
})

记录关键错误将停止测试的执行,并使一切崩溃。毕竟,我们希望确保您的代码在部署中没有关键日志,如果发生这种情况,您应该立即注意。

测试派生字段

链到本节

测试派生字段是一项功能(如下面的示例所示),它允许用户在某个实体中设置字段,并且如果另一个实体从第一个实体派生其字段之一,则自动更新该实体。需要注意的重要一点是,当自动更新发生在AS代码不可知的存储中时,需要重新加载第一个实体。

test('Derived fields example test', () => {
let mainAccount = new GraphAccount('12')
mainAccount.save()
let operatedAccount = new GraphAccount('1')
operatedAccount.operators = ['12']
operatedAccount.save()
let nst = new NameSignalTransaction('1234')
nst.signer = '12'
nst.save()
assert.assertNull(mainAccount.get('nameSignalTransactions'))
assert.assertNull(mainAccount.get('operatorOf'))
mainAccount = GraphAccount.load('12')!
assert.i32Equals(1, mainAccount.nameSignalTransactions.length)
assert.stringEquals('1', mainAccount.operatorOf[0])
})

测试动态数据源

链到本节

可以通过模拟dataSource命名空间的context(), address()network() 函数的返回值来测试动态数据源。这些函数当前返回以下内容:context() -返回一个空实体(DataSourceContext),address()返回0x0000000000000000000000000000000000000000, network() - 返回mainnetcreate(...)createWithContext(...)函数被模拟为什么都不做,因此根本不需要在测试中调用它们。返回值的更改可以通过matchstick-as (版本 0.3.0+) 中dataSourceMock命名空间的函数来完成。

示例如下:

首先,我们有以下事件处理程序(它被有意地重新用于展示模拟数据源):

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

注意,dataSourceMock.resetValues()在末尾被调用。这是因为值在更改时会被记住,如果要返回到默认值,则需要重新设置。

测试覆盖率

链到本节

使用Matchstick,子图开发者可以运行一个脚本,计算编写的单元测试的测试覆盖率。

测试覆盖工具非常简单——它接受编译后的测试wasm二进制文件并将其转换为wat文件,然后可以很容易地检查这些文件,以查看submap.yaml中定义的处理程序是否真正被调用。由于代码覆盖率(以及整个测试)在AssemblyScript和WebAssembly中处于非常早期的阶段,Matchstick无法检查分支覆盖率。相反,我们依赖于这样一个断定,即如果调用了给定的处理程序,那么它的事件/函数就会被正确地模拟。

先决条件

链到本节

要运行Matchstick中提供的测试覆盖功能,您需要事先准备以下几件事:

导出处理程序

链到本节

为了让Matchstick检查正在运行的处理程序,需要从测试文件中导出这些处理程序。例如,在我们的示例中,在gravity.test.ts文件中,我们导入了以下处理程序

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

为了使该函数可见(使其包含在 wat 文件中按名称),我们还需要导出它,例如这:

export { handleNewGravatar }

使用方法

链到本节

设置好后,要运行测试覆盖工具,只需运行:

graph test -- -c

您还可以向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).

日志输出中的测试运行持续时间

链到本节

日志输出包括测试运行持续时间。下面是一个示例:

[2022 年 3 月 31 日星期四 13:54:54 +0300] 程序执行时间:42.270 毫秒。

常见编译器错误

链到本节

关键:无法从具有背景的有效模块创建WasmInstance:未知导入:wasi_snapshot_preview1::尚未定义fd_write

这意味着您在代码中使用了console.log,而AssemblyScript不支持此选项。请考虑使用日志API

ERROR TS2554: Expected ? arguments, but got ?.

返回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 ?.

返回新ethereum.Transaction(defaultAddressBytes, defaultBigInt, defaultAddress, defaultAddress, defaultBigInt, defaultBigInt, defaultBigInt, defaultAddressBytes, defaultBigInt);

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

参数不匹配是由graph-ts and matchstick-as不匹配造成的。解决此类问题的最佳方法是将所有内容更新到最新发布的版本。

反馈

链到本节

如果您有任何问题、反馈、特征请求或只是想与我们联系,最好的地方是 Graph Discord,我们有一个专门的 Matchstick 频道,名为 🔥| 单元测试。

编辑

上页
AssemblyScript的常见问题
下页
开发者常见问题
编辑