ユニットテストフレームワーク
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では、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 test
や npm run test
(ローカル、プロジェクトレベルのgraph-cliを使用し、魅力的に動作します。)を使用するようにしてください。そのためには、もちろん "test"
スクリプトを package.json
ファイルに記述する必要がありますが、これは以下のような簡単なものです。
{"name": "demo-subgraph","version": "0.1.0","scripts": {"test": "graph test",...},"dependencies": {"@graphprotocol/graph-cli": "^0.30.0","@graphprotocol/graph-ts": "^0.27.0","matchstick-as": "^0.5.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
を実行したことがある場合、docker build 中に以下のようなエラーが発生することがあります。
送信者からのエラー: xattr node_modules/binary-install-raw/bin/binary-<platform> へのアクセスに失敗しました: パーミッションが拒否されました。
この場合、ルートフォルダに .dockerignore
を作成し、 node_modules/binary-install-raw/bin
を追加してください。
Matchstick は、matchstick.yaml
設定ファイルによって、カスタムテスト、ライブラリ、マニフェストのパスを使用するように設定することができます。
testsFolder: path/to/testslibsFolder: path/to/libsmanifestPath: path/to/subgraph.yaml
Demo Subgraph レポをクローンすることで、このガイドのサンプルを試したり、遊んだりすることができます。
また、「Matchstickを使ってサブグラフのユニットテストを書く方法」のビデオシリーズもご覧ください。
IMPORTANT: Requires matchstick-as >=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", () => {...})
ファイル中のどのテストよりも前にコードブロックを実行します。もし beforeAll
が describe
ブロックの中で宣言された場合、その 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", () => {...})})
ファイル内の全てのテストの後にコードブロックを実行します。もし 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 の例を使って、簡単なユニットテストがどのように見えるか見てみましょう。
次のようなハンドラ関数があるとします(さらに、生活を便利にするための2つのヘルパー関数もあります)。
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', () => {//...})
このように様々な形で紐解いてみました。まず最初に、重要なことは、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()
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 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として返されるデフォルトのトランザクションのメタデータを使用することができます。以下の例では、イベントオブジェクトのこれらのフィールドを読み書きする方法を示しています:
// 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,)
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!')})
クリティカルエラーのログを取るとテストの実行が止まり、すべてが飛んでしまいます。コードに重要なログがデプロイされていないことを確認し、もし発生した場合にはすぐに気付く必要があります。
派生フィールドのテストは、(以下の例で示すように)ユーザーがあるエンティティにフィールドを設定し、それが最初のエンティティからフィールドの1つを派生している場合、別のエンティティが自動的に更新されるようにする機能です。重要なのは、自動更新は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()
- 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()が呼び出されていることに注目してください。これは、値が変更されると記憶されるため、デフォルトの値に戻したい場合はリセットする必要があるからです。
マッチスティック を使用すると、サブグラフ開発者は、記述された単体テストのテスト カバレッジを計算するスクリプトを実行できます。
テスト カバレッジ ツールは、コンパイルされたテスト 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 -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).
ログ出力にテスト実行時間が含まれるようになりました。以下はその例です:
[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-ts
とmatchstick-as
の不一致によって起こります。このような問題を解決する最善の方法は、すべてを最新のリリース版にアップデートすることです。
質問、フィードバックなどがありましたら、The Graph DiscordにMatchstick専用のチャンネル🔥| unit-testing がありますので、そちらにお問い合わせください。