1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

Goで実装したHyperledger FabricのChaincodeを単体テストしたい

はじめに

Hyperledger FabricのChaincodeをネットワークにデプロイする前に、機能チェックしておきたい。

ということで、Go初学者がFabricのChaincodeを題材にして、Goで単体テストする方法を書き残しました。

環境

  • Hyperledger Fabric v2.2.0
    • サンプルソース、バイナリツール、Dockerイメージのインストール方法はこちら
  • Go 1.15.8
  • Visual Studio Codeのおすすめ拡張機能

注意

本記事では、Chaincode開発用のGoパッケージとしてshimの利用を前提としていますが、v2.0以降ではshimより高水準なAPIとしてcontractapiが提供されています。
今後はこちらを使っていく方が望ましいかもしれません。

(参考)
What is the difference between fabric-chaincode-go and fabric-contract-api-go?
https://stackoverflow.com/questions/60900762/what-is-the-difference-between-fabric-chaincode-go-and-fabric-contract-api-go

準備知識

Goのパッケージについて

  • FabricのChaincode開発ではshimパッケージが提供するAPIを使います。

  • Goでは依存パッケージをimportで指定します。


import (
    "github.com/hyperledger/fabric-chaincode-go/shim"
)
  • コマンド$go mod tidyで、importで記載したパッケージをダウンロードできます。このときgo.modgo.sumという2つのファイルも更新されます。
    • go.modは依存パッケージを管理するファイル。
    • go.sumはパッケージに更新があったかを判断するためのファイルで、依存モジュールのチェックサムが記録される。
  • パッケージ自体は$GOPATH/pkg/mod配下に格納されます。
  • コマンド$go getでも任意のパッケージを引数に指定してダウンロードできます。ただし、$go mod tidyの方がimportで記載した依存パッケージに応じて過不足なくダウンロードできる点が良さそうです。

Goのテストについて

  • Goは標準で自動テストが組み込まれています。(コマンド$go test)
  • カバレッジやベンチマーク測定なども、オプションで組み込まれています。(-cover-bench
  • テストする上で命名規則が決まってます。
    • XXX.goがテスト対象の場合、テストプログラムのファイル名はXXX_test.goとする。
    • テスト関数名の接頭にTestをつける。
  • テストプログラムはテスト対象と同じパッケージにします。XXX.goと同ディレクトリにXXX_test.goを置いておくのが吉。

(参考)
はじめてのGoTest(Qiita)
https://qiita.com/marnie_ms4/items/e51cc6d879cc9ad07af3

GoによるChaincode開発/単体テスト

既に述べた通り、Chaincode開発ではshimパッケージを使います。
そして、Chaincodeの単体テストではshimtestパッケージを使います。

Fabric v1.4.xでの注意点

Fabric v1.4.xではパッケージ体系が異なるのでご注意ください。
Fabric v2.x以降から変更があったようです。

また、v1.4.xではshimに単体テスト用のAPIも含まれていて、shimtestは提供されていません。

v1.4.xで開発/単体テストする場合は、バージョンを指定してパッケージをダウンロードしましょう。

$ go mod init ...
$ go get github.com/hyperledger/fabric/core/chaincode/shim@v1.4

(参考)
Work with Hyperledger Fabric Chaincode Development Tools #4415
https://github.com/SAPDocuments/Tutorials/issues/4415

実践

ここではmarbles02(Tag v2.2.0)の単体テストを実行する方法を記します。(カバレッジ100%を目指すものではありません)

サンプルコードを使うのでテスト対象のmarbles_chaincode.goは実装完了しているものとします。

単体テスト

marbles_chaincode.goと同じディレクトリに、命名規則に従ってテストプログラム(空ファイル)を作成します。

$ cd fabric-samples/chaincode/marbles02/go/
$ touch marbles_chaincode_test.go

一部メソッドのみを対象とした、簡易なテストコードを実装します。
Chaincodeの実装ではバイト配列を扱うことが多いので、標準パッケージの他にも幾つかimportしてます。

marbles_chaincode.go

package main

import (
    "strconv" // 文字列変換を扱う標準パッケージ
    "strings" // 文字列を扱う標準パッケージ
    "testing" // テスト用の関数・構造体を扱う標準パッケージ

    simplejson "github.com/bitly/go-simplejson" // JSON型式を扱うパッケージ
    "github.com/stretchr/testify/assert"        // アサーション機能を扱うパッケージ
    funk "github.com/thoas/go-funk"             // Sliceに対する便利機能(Map, Filterなど)を扱うパッケージ

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-chaincode-go/shimtest"
)

var simpleChaincode = new(SimpleChaincode)
var mockStub = shimtest.NewMockStub("Test Creation", simpleChaincode)

func TestInitMarble(t *testing.T) {
    t.Run("success InitMarble()", func(t *testing.T) {
        functionName := "initMarble"
        marbleName := "marble1"
        marbleColor := "blue"
        marbleSize := "35"
        marbleOwner := "tom"
        functionAndArgs := [][]byte{[]byte(functionName), []byte(marbleName), []byte(marbleColor), []byte(marbleSize), []byte(marbleOwner)}

        // Invoke Function
        res := mockStub.MockInvoke("InitMarble001_1", functionAndArgs)
        if res.Status != shim.OK {
            t.Error("Invoke", functionAndArgs, "failed", string(res.Message))
            t.FailNow()
        } else {
            functionAndArgsMap := funk.Map(functionAndArgs, func(b []byte) string { return string(b) })
            t.Log("Invoke", "(", strings.Join(functionAndArgsMap.([]string), ", "), ")", "successful", string(res.Message))
        }

        // Check StateDB
        bytes, err := mockStub.GetState(marbleName)
        if err != nil {
            t.Error("Failed to get state")
            t.FailNow()
        } else if bytes == nil {
            t.Error("State", marbleName, "failed to get value")
            t.FailNow()
        }

        state, err := simplejson.NewJson(bytes)
        assert.Equal(t, marbleName, state.Get("name").MustString())
        assert.Equal(t, marbleColor, state.Get("color").MustString())
        assert.Equal(t, marbleSize, strconv.Itoa((state.Get("size").MustInt())))
        assert.Equal(t, marbleOwner, state.Get("owner").MustString())
    })
}

func TestReadMarble(t *testing.T) {
    t.Run("success ReadMarble()", func(t *testing.T) {
        functionName := "readMarble"
        marbleName := "marble1"
        marbleColor := "blue"
        marbleSize := "35"
        marbleOwner := "tom"
        functionAndArgs := [][]byte{[]byte(functionName), []byte(marbleName)}

        // Invoke Function
        res := mockStub.MockInvoke("ReadMarble001_1", functionAndArgs)
        if res.Status != shim.OK {
            t.Error("Invoke", functionAndArgs, "failed", string(res.Message))
            t.FailNow()
        } else {
            functionAndArgsMap := funk.Map(functionAndArgs, func(b []byte) string { return string(b) })
            t.Log("Invoke", "(", strings.Join(functionAndArgsMap.([]string), ", "), ")", "successful", string(res.Message))
        }

        // Check Response
        payload := res.Payload
        value, _ := simplejson.NewJson(payload)
        assert.Equal(t, marbleName, value.Get("name").MustString())
        assert.Equal(t, marbleColor, value.Get("color").MustString())
        assert.Equal(t, marbleSize, strconv.Itoa((value.Get("size").MustInt())))
        assert.Equal(t, marbleOwner, value.Get("owner").MustString())
    })
}

依存パッケージダウンロード

テストコードを書いたら依存パッケージをダウンロードします。
go.modがなければ$ go mod initで作成する必要があります。
ただし、marbles02(Tag v2.2.0)では既にgo.modも含まれているため省略します。
$ go mod tidyコマンドで必要なパッケージをダウンロードしましょう。

$ go mod tidy

テスト実行

コマンド$ go testでテスト実行です。

$ go test
> invoke is running initMarble
> - start init marble
> - end init marble
> invoke is running readMarble
> PASS
> ok      github.com/hyperledger/fabric-samples/chaincode/marbles02/go    0.006s

カバレッジ取得

カバレッジ結果はファイルに出力できます。
HTMLに変換することもでき、カバーされている箇所を見やすくできます。

$ go test -coverprofile=cover.out .
> ok      github.com/hyperledger/fabric-samples/chaincode/marbles02/go    0.005s  coverage: 15.0% of statements
$ go tool cover -html=cover.out -o cover.html

(参考)
Go でコードカバレッジを取得する
https://qiita.com/kkohtaka/items/965fe08821cda8c9da8a

Visual Studio Codeなら、html-preview-vscodeという拡張機能でHTMLをプレビューできます。ブラウザ要らずで便利です。

Some content has been disabled in this documentというメッセージが表示される場合は、「change Preview Security Settings」を「Disable」に設定しましょう。

(参考)
VSCodeのhtmlPreviewでscriptタブの箇所がうまく表示されない場合の解決方法
https://entlife.net/2020/10/26/vscode%E3%81%AEhtmlpreview%E3%81%A7script%E3%82%BF%E3%83%96%E3%81%AE%E7%AE%87%E6%89%80%E3%81%8C%E3%81%86%E3%81%BE%E3%81%8F%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88/

おわり

個人的にはGoの勉強になりました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?