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

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

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

# 環境
* Hyperledger Fabric v2.2.0
    * サンプルソース、バイナリツール、Dockerイメージのインストール方法は[こちら](https://hyperledger-fabric.readthedocs.io/en/release-2.2/install.html)
* Go 1.15.8
* Visual Studio Codeのおすすめ拡張機能
    * [Go for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=golang.Go)

### 注意
本記事では、Chaincode開発用のGoパッケージとして[shim](https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim)の利用を前提としていますが、v2.0以降ではshimより高水準なAPIとして[contractapi](https://pkg.go.dev/github.com/hyperledger/fabric-contract-api-go@v1.1.1/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](https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim)パッケージが提供するAPIを使います。

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

```golang

import (
	"github.com/hyperledger/fabric-chaincode-go/shim"
)
```

* コマンド`$go mod tidy`で、`import`で記載したパッケージをダウンロードできます。このとき`go.mod`と`go.sum`という２つのファイルも更新されます。
    * `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](https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim)パッケージを使います。
そして、Chaincodeの単体テストでは[shimtest](https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shimtest)パッケージを使います。

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

* v1.4.x
    * [github.com/hyperledger/fabric/core/chaincode/shim](https://pkg.go.dev/github.com/hyperledger/fabric/core/chaincode/shim)

* v2.x以降
    * [github.com/hyperledger/fabric-chaincode-go/shim
](https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim)


また、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)](https://github.com/hyperledger/fabric-samples/blob/v2.2.0/chaincode/marbles02/go/marbles_chaincode.go)の単体テストを実行する方法を記します。(カバレッジ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](https://marketplace.visualstudio.com/items?itemName=tht13.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の勉強になりました。