单元测试框架
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
可以使用 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.30.0","@graphprotocol/graph-ts": "^0.27.0","matchstick-as": "^0.5.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实现使用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/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
您可以通过克隆Demo Subgraph repo来尝试并使用本指南中的示例。
此外,您还可以查看“如何使用Matchstick为子图编写单元测试”系列视频。
重要事项:需要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
中的代码将在文件中的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)
让我们看看使用Demo Subgraph中的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()
使用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()
- 返回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()在末尾被调用。这是因为值在更改时会被记住,如果要返回到默认值,则需要重新设置。
使用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不支持此选项。请考虑使用日志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 频道,名为 🔥| 单元测试。