单元测试框架
Reading time: 32 min
Matchstick 是一个单元测试框架,由开发,使子图开发者能够在沙盒环境中测试他们的映射逻辑,并放心地部署他们的子图。
为了使用测试帮助器方法并运行测试,您需要安装以下依赖项:
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
可以使用 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 test
或 npm run test
(这将使用本地的项目级的 graph-cli 实例,它的工作原理非常有趣。)为此,您当然需要在 package.json
文件中有一个“ test”
脚本,它可以简单到
{"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,只需打开一个终端,导航到项目的根文件夹,然后简单地运行graph test [options] <datasource>
-它下载最新的Matchstick二进制文件,并在测试文件夹中运行指定的测试或所有测试(如果未指定数据源标志,则运行所有现有测试)。
这将运行测试文件夹中的所有测试:
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
从graph cli 0.25.2
中,graph test
命令支持在带有-d
标志的docker容器中运行matchstick
。docker实现使用,因此它不必在每次执行graph test-d
命令时重新构建docker映像。或者,您可以按照存储库中的说明手动运行docker。
❗ graph test -d
forces docker run
to run with flag -t
. This must be removed to run inside non-interactive environments (like GitHub CI).
❗如果您以前运行过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/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
IMPORTANT: The test structure described below depens on matchstick-as
version >=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
中的代码将在文件中的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
在describe
区块内声明,它将在该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
在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')// code that should update the displayName to 1st Gravatarassert.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/0x0assert.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")// code that should update the displayName to 1st Gravatarassert.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/0x0assert.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 Gravatarassert.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/0x0assert.fieldEquals("Gravatar", "0x0", "imageUrl", "https://www.gravatar.com/avatar/0x0")})})
fieldEquals(entityType: string, id: string, fieldName: string, expectedVal: string)equals(expected: ethereum.Value, actual: ethereum.Value)notInStore(entityType: string, id: string)addressEquals(address1: Address, address2: Address)bytesEquals(bytes1: Bytes, bytes2: Bytes)i32Equals(number1: i32, number2: i32)bigIntEquals(bigInt1: BigInt, bigInt2: BigInt)booleanEquals(bool1: boolean, bool2: boolean)stringEquals(string1: string, string2: string)arrayEquals(array1: Array<ethereum.Value>, array2: Array<ethereum.Value>)tupleEquals(tuple1: ethereum.Tuple, tuple2: ethereum.Tuple)assertTrue(value: boolean)assertNull<T>(value: T)assertNotNull<T>(value: T)entityCount(entityType: string, expectedCount: i32)
As of version 0.6.0, asserts support custom error messages as well
assert.fieldEquals('Gravatar', '0x123', 'id', '0x123', 'Id should be 0x123')assert.equals(ethereum.Value.fromI32(1), ethereum.Value.fromI32(1), 'Value should equal 1')assert.notInStore('Gravatar', '0x124', 'Gravatar should not be in store')assert.addressEquals(Address.zero(), Address.zero(), 'Address should be zero')assert.bytesEquals(Bytes.fromUTF8('0x123'), Bytes.fromUTF8('0x123'), 'Bytes should be equal')assert.i32Equals(2, 2, 'I32 should equal 2')assert.bigIntEquals(BigInt.fromI32(1), BigInt.fromI32(1), 'BigInt should equal 1')assert.booleanEquals(true, true, 'Boolean should be true')assert.stringEquals('1', '1', 'String should equal 1')assert.arrayEquals([ethereum.Value.fromI32(1)], [ethereum.Value.fromI32(1)], 'Arrays should be equal')assert.tupleEquals(changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),changetype<ethereum.Tuple>([ethereum.Value.fromI32(1)]),'Tuples should be equal',)assert.assertTrue(true, 'Should be true')assert.assertNull(null, 'Should be null')assert.assertNotNull('not null', 'Should be not null')assert.entityCount('Gravatar', 1, 'There should be 2 gravatars')assert.dataSourceCount('GraphTokenLockWallet', 1, 'GraphTokenLockWallet template should have one data source')assert.dataSourceExists('GraphTokenLockWallet',Address.zero().toHexString(),'GraphTokenLockWallet should have a data source for zero address',)
让我们看看使用中的Gravatar示例进行简单的单元测试的样子。
假设我们有以下处理程序函数(以及两个帮助函数,以使我们的生活更轻松):
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}
我们首先必须在项目中创建一个测试文件。这是一个示例,说明它可能是什么样子的:
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 eventslet newGravatarEvent = createNewGravatarEvent(12345, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')let anotherGravatarEvent = createNewGravatarEvent(3546, '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', 'cap', 'pac')// Call mapping functions passing the events we just createdhandleNewGravatars([newGravatarEvent, anotherGravatarEvent])// Assert the state of the storeassert.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 slateclearStore()})test('Next test', () => {//...})
这太多了!首先,需要注意的一件重要事情是,我们将从matchstick-as
中导入东西,作为我们的AssemblyScript助手库(作为npm模块分发)。您可以在找到存储库。matchstick as
为我们提供了有用的测试方法,还定义了我们将用来构建测试块的test()
函数。剩下的部分很简单——下面是发生的事情:
- 我们正在设置我们的初始状态并添加一个自定义的 Gravatar 实体。
- 我们使用
createNewGravatarEvent()
函数定义了两个NewGravatar
事件对象以及它们的数据。 - 我们正在为这些事件调用处理方法-
-handleNewGravatars()
,并传入我们的自定义事件列表。 - 我们断定存储的状态。那是怎么实现的呢?- 我们传递一个实体类型和 id 的唯一组合。然后我们检查该实体的一个特定字段,并断定它具有我们期望的值。我们为我们添加到存储的初始 Gravatar 实体,以及当处理函数被调用时被添加的两个 Gravatar 实体都做这个。
- 最后--我们用
clearStore()
清理存储,这样我们的下一个测试就可以从一个新的空存储对象开始。我们可以定义任意多的测试块。
好了,我们创建了第一个测试!👏
现在,为了运行我们的测试,您只需在子图根文件夹中运行以下命令:
graph test Gravity
如果一切顺利,您应该会收到以下信息:
用户能够用一组已知的实体来补充存储。下面是一个用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()
用户可以使用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 itexport { 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 callbackexport function processGravatar(value: JSONValue, userData: Value): void {// See the JSONValue documentation for details on dealing// with JSON valueslet obj = value.toObject()let id = obj.get('id')if (!id) {return}// Callbacks can also created entitieslet gravatar = new Gravatar(id.toString())gravatar.displayName = userData.toString() + id.toString()gravatar.save()}// function that calls 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()}
用户能够通过断定实体断定存储的最终(或中途)状态。为此,用户必须提供实体类型、实体的特定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对象上的这些字段:
// Readlet logType = newGravatarEvent.logType// Writelet 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()
As of version 0.6.0, logStore
no longer prints derived fields, instead users can use the new logEntity
function. Of course logEntity
can be used to print any entity, not just ones that have derived fields. logEntity
takes the entity type, entity id and a showRelated
flag to indicate if users want to print the related derived entities.
import { logEntity } from 'matchstick-as/assembly/store'logEntity("Gravatar", 23, true)
使用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!')})
记录关键错误将停止测试的执行,并使一切崩溃。毕竟,我们希望确保您的代码在部署中没有关键日志,如果发生这种情况,您应该立即注意。
Testing derived fields is a feature which allows users to set a field on a certain entity and have another entity be updated automatically if it derives one of its fields from the first entity.
Before version 0.6.0
it was possible to get the derived entities by accessing them as entity fields/properties, like so:
let entity = ExampleEntity.load('id')let derivedEntity = entity.derived_entity
As of version 0.6.0
, this is done by using the loadRelated
function of graph-node, the derived entities can be accessed the same way as in the handlers.
test('Derived fields example test', () => {let mainAccount = GraphAccount.load('12')!assert.assertNull(mainAccount.get('nameSignalTransactions'))assert.assertNull(mainAccount.get('operatorOf'))let operatedAccount = GraphAccount.load('1')!operatedAccount.operators = [mainAccount.id]operatedAccount.save()mockNameSignalTransaction('1234', mainAccount.id)mockNameSignalTransaction('2', mainAccount.id)mainAccount = GraphAccount.load('12')!assert.assertNull(mainAccount.get('nameSignalTransactions'))assert.assertNull(mainAccount.get('operatorOf'))const nameSignalTransactions = mainAccount.nameSignalTransactions.load()const operatorsOfMainAccount = mainAccount.operatorOf.load()assert.i32Equals(2, nameSignalTransactions.length)assert.i32Equals(1, operatorsOfMainAccount.length)assert.stringEquals('1', operatorsOfMainAccount[0].id)mockNameSignalTransaction('2345', mainAccount.id)let nst = NameSignalTransaction.load('1234')!nst.signer = '11'nst.save()store.remove('NameSignalTransaction', '2')mainAccount = GraphAccount.load('12')!assert.i32Equals(1, mainAccount.nameSignalTransactions.load().length)})
As of version 0.6.0
, users can test loadInBlock
by using the mockInBlockStore
, it allows mocking entities in the block cache.
import { afterAll, beforeAll, describe, mockInBlockStore, test } from 'matchstick-as'import { Gravatar } from '../../generated/schema'describe('loadInBlock', () => {beforeAll(() => {mockInBlockStore('Gravatar', 'gravatarId0', gravatar)})afterAll(() => {clearInBlockStore()})test('Can use entity.loadInBlock() to retrieve entity from cache store in the current block', () => {let retrievedGravatar = Gravatar.loadInBlock('gravatarId0')assert.stringEquals('gravatarId0', retrievedGravatar!.get('id')!.toString())})test("Returns null when calling entity.loadInBlock() if an entity doesn't exist in the current block", () => {let retrievedGravatar = Gravatar.loadInBlock('IDoNotExist')assert.assertNull(retrievedGravatar)})})
可以通过模拟dataSource命名空间的context()
, address()
和 network()
函数的返回值来测试动态数据源。这些函数当前返回以下内容:context()
-返回一个空实体(DataSourceContext),address()
返回0x0000000000000000000000000000000000000000
, network()
- 返回mainnet
。create(...)
和 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()在末尾被调用。这是因为值在更改时会被记住,如果要返回到默认值,则需要重新设置。
As of version 0.6.0
, it is possible to test if a new data source has been created from a template. This feature supports both ethereum/contract and file/ipfs templates. There are four functions for this:
assert.dataSourceCount(templateName, expectedCount)
can be used to assert the expected count of data sources from the specified templateassert.dataSourceExists(templateName, address/ipfsHash)
asserts that a data source with the specified identifier (could be a contract address or IPFS file hash) from a specified template was createdlogDataSources(templateName)
prints all data sources from the specified template to the console for debugging purposesreadFile(path)
reads a JSON file that represents an IPFS file and returns the content as Bytes
test('ethereum/contract dataSource creation example', () => {// Assert there are no dataSources created from GraphTokenLockWallet templateassert.dataSourceCount('GraphTokenLockWallet', 0)// Create a new GraphTokenLockWallet datasource with address 0xA16081F360e3847006dB660bae1c6d1b2e17eC2AGraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))// Assert the dataSource has been createdassert.dataSourceCount('GraphTokenLockWallet', 1)// Add a second dataSource with contextlet context = new DataSourceContext()context.set('contextVal', Value.fromI32(325))GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)// Assert there are now 2 dataSourcesassert.dataSourceCount('GraphTokenLockWallet', 2)// Assert that a dataSource with address "0xA16081F360e3847006dB660bae1c6d1b2e17eC2B" was created// Keep in mind that `Address` type is transformed to lower case when decoded, so you have to pass the address as all lower case when asserting if it existsassert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())logDataSources('GraphTokenLockWallet')})
🛠 {"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {"kind": "ethereum/contract","name": "GraphTokenLockWallet","address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a","context": null},"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {"kind": "ethereum/contract","name": "GraphTokenLockWallet","address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b","context": {"contextVal": {"type": "Int","data": 325}}}}
Similarly to contract dynamic data sources, users can test test file datas sources and their handlers
...templates:- kind: file/ipfsname: GraphTokenLockMetadatanetwork: mainnetmapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/token-lock-wallet.tshandler: handleMetadataentities:- TokenLockMetadataabis:- name: GraphTokenLockWalletfile: ./abis/GraphTokenLockWallet.json
"""Token Lock Wallets which hold locked GRT"""type TokenLockMetadata @entity {"The address of the token lock wallet"id: ID!"Start time of the release schedule"startTime: BigInt!"End time of the release schedule"endTime: BigInt!"Number of periods between start time and end time"periods: BigInt!"Time when the releases start"releaseStartTime: BigInt!}
{"startTime": 1,"endTime": 1,"periods": 1,"releaseStartTime": 1}
export function handleMetadata(content: Bytes): void {// dataSource.stringParams() returns the File DataSource CID// stringParam() will be mocked in the handler test// for more info https://thegraph.com/docs/en/developing/creating-a-subgraph/#create-a-new-handler-to-process-fileslet tokenMetadata = new TokenLockMetadata(dataSource.stringParam())const value = json.fromBytes(content).toObject()if (value) {const startTime = value.get('startTime')const endTime = value.get('endTime')const periods = value.get('periods')const releaseStartTime = value.get('releaseStartTime')if (startTime && endTime && periods && releaseStartTime) {tokenMetadata.startTime = startTime.toBigInt()tokenMetadata.endTime = endTime.toBigInt()tokenMetadata.periods = periods.toBigInt()tokenMetadata.releaseStartTime = releaseStartTime.toBigInt()}tokenMetadata.save()}}
import { assert, test, dataSourceMock, readFile } from 'matchstick-as'import { Address, BigInt, Bytes, DataSourceContext, ipfs, json, store, Value } from '@graphprotocol/graph-ts'import { handleMetadata } from '../../src/token-lock-wallet'import { TokenLockMetadata } from '../../generated/schema'import { GraphTokenLockMetadata } from '../../generated/templates'test('file/ipfs dataSource creation example', () => {// Generate the dataSource CID from the ipfsHash + ipfs path file// For example QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.jsonconst ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'const CID = `${ipfshash}/example.json`// Create a new dataSource using the generated CIDGraphTokenLockMetadata.create(CID)// Assert the dataSource has been createdassert.dataSourceCount('GraphTokenLockMetadata', 1)assert.dataSourceExists('GraphTokenLockMetadata', CID)logDataSources('GraphTokenLockMetadata')// Now we have to mock the dataSource metadata and specifically dataSource.stringParam()// dataSource.stringParams actually uses the value of dataSource.address(), so we will mock the address using dataSourceMock from matchstick-as// First we will reset the values and then use dataSourceMock.setAddress() to set the CIDdataSourceMock.resetValues()dataSourceMock.setAddress(CID)// Now we need to generate the Bytes to pass to the dataSource handler// For this case we introduced a new function readFile, that reads a local json and returns the content as Bytesconst content = readFile(`path/to/metadata.json`)handleMetadata(content)// Now we will test if a TokenLockMetadata was createdconst metadata = TokenLockMetadata.load(CID)assert.bigIntEquals(metadata!.endTime, BigInt.fromI32(1))assert.bigIntEquals(metadata!.periods, BigInt.fromI32(1))assert.bigIntEquals(metadata!.releaseStartTime, BigInt.fromI32(1))assert.bigIntEquals(metadata!.startTime, BigInt.fromI32(1))})
使用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 -cSkipping download/install step because binary already exists at /Users/petko/work/demo-subgraph/node_modules/binary-install-raw/bin/0.4.0___ ___ _ _ _ _ _| \/ | | | | | | | (_) | || . . | __ _| |_ ___| |__ ___| |_ _ ___| | __| |\/| |/ _` | __/ __| '_ \/ __| __| |/ __| |/ /| | | | (_| | || (__| | | \__ \ |_| | (__| <\_| |_/\__,_|\__\___|_| |_|___/\__|_|\___|_|\_\Compiling...Running in coverage report mode.️Reading generated test modules... 🔎️Generating coverage report 📝Handlers for source 'Gravity':Handler 'handleNewGravatar' is tested.Handler 'handleUpdatedGravatar' is not tested.Handler 'handleCreateGravatar' is tested.Test coverage: 66.7% (2/3 handlers).Handlers for source 'GraphTokenLockWallet':Handler 'handleTokensReleased' is not tested.Handler 'handleTokensWithdrawn' is not tested.Handler 'handleTokensRevoked' is not tested.Handler 'handleManagerUpdated' is not tested.Handler 'handleApproveTokenDestinations' is not tested.Handler 'handleRevokeTokenDestinations' is not tested.Test coverage: 0.0% (0/6 handlers).Global test coverage: 22.2% (2/9 handlers).
日志输出包括测试运行持续时间。下面是一个示例:
[2022 年 3 月 31 日星期四 13:54:54 +0300] 程序执行时间:42.270 毫秒。
关键:无法从具有背景的有效模块创建WasmInstance:未知导入:wasi_snapshot_preview1::尚未定义fd_write
这意味着您在代码中使用了console.log
,而AssemblyScript不支持此选项。请考虑使用
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 频道,名为 🔥| 单元测试。