現像 > ユニットテストフレームワーク

ユニットテストフレームワーク

Reading time: 35 min

Matchstick はLimeChainが開発したユニットテストフレームワークで、サブグラフの開発者がサンドボックス環境でマッピングロジックをテストし、自信を持ってサブグラフをデプロイすることができます!

はじめに

このセクションへのリンク

依存関係のインストール

このセクションへのリンク

テストヘルパーメソッドを使用し、テストを実行するためには、以下の依存関係をインストールする必要があります:

yarn add --dev matchstick-as

graph-node はPostgreSQLに依存しているので、もしまだ持っていなければ、インストールする必要があります。他の方法で追加すると予期しないエラーが発生する可能性があるので、以下のコマンドを使用することを強くお勧めします!

Postgresのインストールコマンド:

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

Postgresのインストールコマンド(お使いのディストロに依存します)。

sudo apt postgresql をインストール

WSL (Linux 用 Windows サブシステム)

このセクションへのリンク

WSLでは、Dockerアプローチとバイナリアプローチの両方でMatchstickを使用することができます。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 をサポートしておらず、これはまだWSL上の新しいUbuntuイメージのデフォルトバージョンになっています。例えばマッチスティックはv18.1.0 でWSL上で動作することが確認されており、nvm を経由するか、グローバルNode.jsを更新すれば切り替えることができます。nodejsを更新したら、node_modulesを削除し、npm installを再度実行するのを忘れないでください! それから、libpq がインストールされていることを確認してください。

sudo apt-get install libpq-dev (インストール)

最後に、graph test (グローバルにインストールされたgraph-cliを使用します。なぜかWSLでは壊れているようです)を使用せず、yarn testnpm 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 をサブグラフ・プロジェクトで使用するには、ターミナルを開き、プロジェクトのルート・フォルダに移動して、 graph test [options] <datasource> と実行するだけで、最新の Matchstick バイナリがダウンロードされて、テスト・フォルダにある指定したテストまたは全てのテストが実行されます (datasource flag が指定されていなければ既存の全てのテスト).

これにより、test フォルダ内のすべてのテストが実行されます。

グラフテスト

これは、gravity.test.tsという名前のテストと、gravityというフォルダの中にあるすべてのテストを実行します:

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 -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 build 中に以下のようなエラーが発生することがあります。

送信者からのエラー: xattr node_modules/binary-install-raw/bin/binary-<platform> へのアクセスに失敗しました: パーミッションが拒否されました。

この場合、ルートフォルダに .dockerignore を作成し、 node_modules/binary-install-raw/bin を追加してください。

コンフィギュレーション

このセクションへのリンク

Matchstick は、matchstick.yaml 設定ファイルによって、カスタムテスト、ライブラリ、マニフェストのパスを使用するように設定することができます。

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

デモ・サブグラフ

このセクションへのリンク

Demo Subgraph レポをクローンすることで、このガイドのサンプルを試したり、遊んだりすることができます。

ビデオチュートリアル

このセクションへのリンク

また、「Matchstickを使ってサブグラフのユニットテストを書く方法」のビデオシリーズもご覧ください。

IMPORTANT: The test structure described below depens on matchstick-as version >=0.5.0

describe(name: String , () => {}) - Defines a test group.

注:

  • ディスクリートは必須ではありません。describe() ブロックの外側で test() を旧来の方法で使用することができます。

例:

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

Nested describe() example:

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", () => {
...
})

ファイル中のどのテストよりも前にコードブロックを実行します。もし beforeAlldescribe ブロックの中で宣言された場合、その describe ブロックの先頭で実行されます。

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", () => {
...
})
})

ファイル内の全てのテストの後にコードブロックを実行します。もし 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", () => {
...
})
})

各テストの前にコードブロックを実行します。もし 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')
})
})

各テストの後にコードブロックを実行します。もし 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)

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

単体テストを書く

このセクションへのリンク

Demo Subgraph にある Gravatar の例を使って、簡単なユニットテストがどのように見えるか見てみましょう。

次のようなハンドラ関数があるとします(さらに、生活を便利にするための2つのヘルパー関数もあります)。

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', () => {
//...
})

このように様々な形で紐解いてみました。まず最初に、重要なことは、AssemblyScript のヘルパーライブラリである matchstick-as からインポートしていることです (npm モジュールとして配布されています)。リポジトリはこちらにあります。matchstick-as は便利なテストメソッドを提供し、テストブロックを構築するために使用する test() 関数を定義しています。残りの部分はとても簡単で、次のようなことが起こります。

  • 初期状態を設定し、カスタムGravatarエンティティを1つ追加しています;
  • createNewGravatarEvent() 関数を使用して、2 つの NewGravatar イベント オブジェクトとそれらのデータを定義します。
  • これらのイベントのハンドラメソッド - handleNewGravatars() を呼び出し、カスタムイベントのリストを渡しています;
  • storeの状態をアサートする場合、これはどのように行われるのでしょうか。- Entityの種類とidの一意の組み合わせを渡します。そして、そのEntityの特定のフィールドをチェックし、期待通りの値を持っていることを表明します。これはstoreに追加した最初の Gravatar Entity と、ハンドラ関数が呼び出されたときに追加される 2 つの Gravatar Entity の両方に対して行っているのです。
  • 最後に、clearStore()を使ってストアを掃除し、次のテストが新鮮で空のストア・オブジェクトで始められるようにしています。テストブロックは必要に応じていくつでも定義できます。

これで最初のテストが完成しました! 👏

テストを実行するには、サブグラフのルートフォルダで以下を実行する必要があります:

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

Mocking IPFS files (from matchstick 0.4.1)

このセクションへのリンク

mockIpfsFile(hash, filePath)関数を使用することにより、IPFSファイルのモックを作成することができます。最初の引数はIPFSファイルのハッシュ/パス、2番目の引数はローカルファイルへのパスです。

注意: ipfs.map/ipfs.mapJSON をテストするとき、下記のテスト例の processGravatar() 関数のように、コールバック関数は matchstck がそれを検出するためにテストファイルからエクスポートされなければなりません。

.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として返されるデフォルトのトランザクションのメタデータを使用することができます。以下の例では、イベントオブジェクトのこれらのフィールドを読み書きする方法を示しています:

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

Printing the whole store, or single entities from it (for debug purposes)

このセクションへのリンク

このヘルパー関数を使って、ストア全体をコンソールに出力することができます:

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

ShouldFail = true とマークされているにもかかわらずテストが失敗した場合は、ログにエラーとして表示され、テストブロックは失敗します。また、shouldFail = false (デフォルトの状態) と設定されている場合は、テスト実行者がクラッシュします。

ユニットテストにカスタムログを持たせることは、マッピングにログを持たせることと全く同じです。違いは、ログオブジェクトをgraph-tsではなくmatchstick-asからインポートする必要があることです。以下は、すべての非重要なログタイプを使った簡単な例です:

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() - 0x000000000000000000000000 を返す、 network() - mainnet を返す、です。create(...)createWithContext(...)関数は何もしないようにモックされているので、テストの中で呼ばれる必要は全くないでしょう。戻り値の変更は matchstick-as (version 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 namespaceのメソッドの1つを使用して、すべての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()が呼び出されていることに注目してください。これは、値が変更されると記憶されるため、デフォルトの値に戻したい場合はリセットする必要があるからです。

Testing dynamic data source creation

このセクションへのリンク

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 template
  • assert.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 created
  • logDataSources(templateName) prints all data sources from the specified template to the console for debugging purposes
  • readFile(path) reads a JSON file that represents an IPFS file and returns the content as Bytes

Testing ethereum/contract templates

このセクションへのリンク
test('ethereum/contract dataSource creation example', () => {
// Assert there are no dataSources created from GraphTokenLockWallet template
assert.dataSourceCount('GraphTokenLockWallet', 0)
// Create a new GraphTokenLockWallet datasource with address 0xA16081F360e3847006dB660bae1c6d1b2e17eC2A
GraphTokenLockWallet.create(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2A'))
// Assert the dataSource has been created
assert.dataSourceCount('GraphTokenLockWallet', 1)
// Add a second dataSource with context
let context = new DataSourceContext()
context.set('contextVal', Value.fromI32(325))
GraphTokenLockWallet.createWithContext(Address.fromString('0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'), context)
// Assert there are now 2 dataSources
assert.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 exists
assert.dataSourceExists('GraphTokenLockWallet', '0xA16081F360e3847006dB660bae1c6d1b2e17eC2B'.toLowerCase())
logDataSources('GraphTokenLockWallet')
})
Example logDataSource output
このセクションへのリンク
🛠 {
"0xa16081f360e3847006db660bae1c6d1b2e17ec2a": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
"context": null
},
"0xa16081f360e3847006db660bae1c6d1b2e17ec2b": {
"kind": "ethereum/contract",
"name": "GraphTokenLockWallet",
"address": "0xa16081f360e3847006db660bae1c6d1b2e17ec2b",
"context": {
"contextVal": {
"type": "Int",
"data": 325
}
}
}
}

Testing file/ipfs templates

このセクションへのリンク

Similarly to contract dynamic data sources, users can test test file datas sources and their handlers

...
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
Example 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!
}
{
"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-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', () => {
// Generate the dataSource CID from the ipfsHash + ipfs path file
// For example QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm/example.json
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
const CID = `${ipfshash}/example.json`
// Create a new dataSource using the generated CID
GraphTokenLockMetadata.create(CID)
// Assert the dataSource has been created
assert.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 CID
dataSourceMock.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 Bytes
const content = readFile(`path/to/metadata.json`)
handleMetadata(content)
// Now we will test if a TokenLockMetadata was created
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))
})

テストカバレッジ

このセクションへのリンク

マッチスティック を使用すると、サブグラフ開発者は、記述された単体テストのテスト カバレッジを計算するスクリプトを実行できます。

テスト カバレッジ ツールは、コンパイルされたテスト wasm バイナリを取得して、それらを wat ファイルに変換します。このファイルは、subgraph.yaml で定義されたハンドラーが呼び出されているかどうかを簡単に検査して確認できます。 AssemblyScript と WebAssembly ではコード カバレッジ (およびテスト全体) が非常に初期段階にあるため、Matchstick はブランチ カバレッジをチェックできません。代わりに、特定のハンドラーが呼び出された場合、そのイベント/関数が適切にモック化されているというアサーションに依存します。

Matchstick で提供されているテストカバレッジ機能を実行するには、事前に準備しておくことがいくつかあります:

ハンドラのエクスポート

このセクションへのリンク

Matchstick がどのハンドラが実行されているかをチェックするために、それらのハンドラは test file からエクスポートされる必要があります。例えばこの例では、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.

一般的なコンパイラーエラー

このセクションへのリンク

Critical: 有効なモジュールから WasmInstance を作成できない。コンテキストが不明 インポート: wasi_snapshot_preview1::fd_write が定義されていない

これは、コード内でconsole.logを使用していることを意味し、AssemblyScriptではサポートされていません。Logging API の利用をご検討ください。

ERROR TS2554: 期待された引数は?

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: 期待された引数は?

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

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

引数の不一致は、graph-tsmatchstick-asの不一致によって起こります。このような問題を解決する最善の方法は、すべてを最新のリリース版にアップデートすることです。

質問、フィードバックなどがありましたら、The Graph DiscordにMatchstick専用のチャンネル🔥| unit-testing がありますので、そちらにお問い合わせください。

ページを編集

AssemblyScriptのよくある問題
開発者 FAQ
ページを編集