天天看點

Fabric 2.0,go鍊碼單元測試1.引入必要依賴2.使用3.為atcc鍊碼寫一個單元測試

之前一直是完成鍊碼的邏輯,然後打包部署在fabric網絡之後,才知道鍊碼寫的正不正确,但是這樣返工一方面浪費時間,另一方面,在開發時心底也是虛的。

比較理想的開發方法是首先為接口寫好自動化測試,運作,出錯,然後再去開發代碼,來通過測試用例之後才算完成開發,這也是一種測試驅動開發的思想,好處就是之後即使修改代碼也可以很友善的完成回歸測試,再配合git,就可以更大膽的進行開發了。

在fabric環境下進行測試的話一個難點在于上下文環境的模拟,但是關于這點,事實上官方給出了一個測試編寫的樣例,如果是最2.3.0的fabric,可以在fabric-samples/asset-transfer-basic/chaincode-go/chaincode/下找到這個smartcontract_test.go檔案,如果熟悉Java的spring開發架構的話,可以類比到對Spring架構進行測試時使用的Mock類,它提供了一個虛拟的賬本互動環境,可以供我們模拟賬本調用并且手動抛出錯誤等,這樣就可以用go自帶的單元測試功能來測試鍊碼的功能。本文接下來首先會參考smartcontract_test.go總結一下編寫測試的套路,然後使用這個架構對我們之前使用的鍊碼編寫一下單元測試。

1.引入必要依賴

測試檔案首先需要寫包名和必要的依賴,首先包名與被測試的鍊碼有關,比如被測試的鍊碼使用的包名為chaincode,那麼測試檔案的包名就必須為chaincode_test,否則運作測試會報錯,這點應該是go的測試規定的,依賴的話主要是四個方面,這裡分别來介紹一下。

第一是go自帶的相關依賴,包括json的序列化工具包"encoding/json"格式化輸入輸出"fmt"以及自帶的單元測試架構"testing"。

第二種就是fabric所提供的運作環境包,包括"github.com/hyperledger/fabric-chaincode-go/shim"、“github.com/hyperledger/fabric-contract-api-go/contractapi”、“github.com/hyperledger/fabric-protos-go/ledger/queryresult”。

第三種是不屬于go自帶也不是fabric官方提供的包,在這裡隻有這個"github.com/stretchr/testify/require",他提供了一種斷言機制,可以類比為JUnit的Assert。

最後一種是引用目前目錄下的包,這裡有兩個,第一個是"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode",也就是被測試的鍊碼,第二個是"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode/mocks",是官方提供的對上下文環境的模拟實作,看注釋這幾個檔案似乎是自動生成的,有上千行,使用時直接複制過去改一下引用之後用就好了。

作為go的初學者,我當時看到"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode"這種引用結構,以為這兩個包是從網絡上現下的,但是通過檢視go.mod,可以看到下面一句話:

module github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go
           

這個module可以當做一個絕對路徑,可以試一下去掉這裡和代碼裡的-go部分,測試同樣可以跑通。

之後代碼中還有一個接口組合,比如将shim.ChaincodeStubInterface的所有方法都加到chaincodeStub接口上建立出一個等效接口,這裡我實在看不懂他的用法,而事實上,把他們注釋掉也不影響測試,移除時還會順手把fabric運作 環境包一起移除了,如果有看懂用法的同學歡迎評論區留言,我這裡的猜測是嚴謹起見保證接口的一緻性,即使是在測試類中的接口也要和fabric官方的接口保持一緻。

保留必須包之後的代碼如下:

package chaincode_test
import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
	"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode"
	"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode/mocks"
	"github.com/stretchr/testify/require"
)
           

2.使用

2.1.概述

Mocks提供了鍊碼中的GetState、PutState等方法的對應的傳回值設定方法GetStateReturns和PutStateReturns方法等用于設定鍊碼中調用GetState等方法的傳回值及錯誤情況等,如果把鍊碼類比為service層,那麼這裡我們可以把我們的傳回值手動設定看作service層調用了我們自己實作的dao層,由此可以實作對service層的間接控制,是以到這裡我們也可以看出來,其實鍊碼測試時是無狀态的,即調用存入的API之後并沒有儲存這個存入記錄的狀态,取不出對應的資料。

在使用之前,我們需要用一些代碼來初始化傳回值設定的樁:

chaincodeStub := &mocks.ChaincodeStub{}
transactionContext := &mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
           

初始化好之後,我們就可以使用chaincodeStub來調用相應的傳回值設定方法。

比如說鍊碼是如下的方法:

// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
	assetJSON, err := ctx.GetStub().GetState(id)
	if err != nil {
		return false, fmt.Errorf("failed to read from world state: %v", err)
	}
	return assetJSON != nil, nil
}
           

那麼相應的,我們可以調用GetStateReturns方法來設定其傳回值:

func TestAssetExists(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	assetTransfer := chaincode.SmartContract{}
	expectedAsset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(expectedAsset)
	require.NoError(t, err)
	// 方法一
	chaincodeStub.GetStateReturns(bytes, nil)
	exist, err := assetTransfer.AssetExists(transactionContext, "asset1")
	require.NoError(t, err)
	require.Equal(t, true, exist)
	// 方法二
	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	_, err = assetTransfer.AssetExists(transactionContext, "asset1")
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}
           

從代碼中可以看出,方法一裡面我們手動設定了傳回值為一個序列化的json對象,是以鍊碼傳回值為true,方法二中我們則手動設定了傳回時産生的錯誤,是以方法執行之後獲得到我們事先設定的錯誤,最後我們的斷言都是用require的方法來實作的。

最後有一個測試起來稍微麻煩點的方法,就是擷取多個資料,因為鍊碼中,是通過範圍查詢傳回疊代器,然後不停的Next來輸出所有資料的,是以在寫測試樁時,傳回的疊代器是我們重寫了HasNext和Next傳回值的版本,是以相對較複雜一些。

首先我們需要定義傳回的疊代器,使用StateQueryIterator{}來進行建立,然後分别設定HasNext和Next方法的傳回值,其中HasNext方法的傳回值通過HasNextReturnsOnCall(times, boolValue)來設定,其中times為第幾次調用HasNext,boolValue為調用的時候傳回的布爾值,如果隻有一次調用,那麼可以直接調用HasNextReturns,傳入布爾值,為下次調用HasNext方法的傳回值。Next方法的傳回值則通過NextReturns方法來設定,第一個參數為下一次調用Next()方法的傳回值,類似隊列,每次設定都是給隊尾加入元素,Next方法調用則是從隊頭拿出元素,第二個參數則為Next方法抛出的錯誤,如果沒有錯誤,則設定為nil。

之後就是設定傳回值了,GetStateByRange方法使用GetStateByRangeReturns方法來設定,第一個參數是一個疊代器,第二個參數則為傳回時産生的錯誤,如果沒有就傳回nil。具體代碼如下:

// 建立傳回的json序列
asset := &chaincode.Asset{ID: "asset1"}
bytes, err := json.Marshal(asset)
require.NoError(t, err)
// 建立疊代器
iterator := &mocks.StateQueryIterator{}
// 設定疊代器有兩個值,第三次HasNext傳回沒有更多
iterator.HasNextReturnsOnCall(0, true)
iterator.HasNextReturnsOnCall(1, true)
iterator.HasNextReturnsOnCall(2, false)
// 設定前兩次有值的時候的傳回值
iterator.NextReturns(&queryresult.KV{Value: bytes}, nil)
iterator.NextReturns(&queryresult.KV{Value: bytes}, nil)
// 建立stub
chaincodeStub := &mocks.ChaincodeStub{}
transactionContext := &mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
// 設定傳回疊代器
chaincodeStub.GetStateByRangeReturns(iterator, nil)
assetTransfer := &chaincode.SmartContract{}
assets, err := assetTransfer.GetAllAssets(transactionContext)
require.NoError(t, err)
// 批量擷取方法應該是兩個asset資産
require.Equal(t, []*chaincode.Asset{asset, asset}, assets)
           

然後我們可以對測試代碼裡出現的API進行一下總結。

2.2.Mock相關API

// 擷取stub對象
chaincodeStub := &mocks.ChaincodeStub{}
transactionContext := &mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
// 設定Get、Put、Del方法的傳回值
chaincodeStub.GetStateReturns(bytes, nil)	// 第一個參數為傳回值,第二個參數為錯誤
chaincodeStub.PutStateReturns(fmt.Errorf("failed inserting key"))	// 參數為錯誤
chaincodeStub.DelStateReturns(nil)	// 參數為錯誤
// 建立與設定疊代器對象
iterator := &mocks.StateQueryIterator{}
iterator.HasNextReturnsOnCall(0, true)	// 第一個參數為調用次數,第二個參數為對應傳回值,和HasNextReturns可以一起用也可以隻用一個
iterator.HasNextReturns(true)	// 設定下一次的HasNext方法傳回值
iterator.NextReturns(&queryresult.KV{Value: bytes}, nil)	// 第一個參數為下一次調用Next的傳回值,第二個參數為調用時産生的錯誤
// 設定GetStateByRange方法的傳回值。
chaincodeStub.GetStateByRangeReturns(iterator, nil)	// 第一個參數為疊代器,第二個參數為傳回時産生的錯誤
           

2.3.斷言相關API

// 錯誤相關的斷言
require.NoError(t, err)	// 不能産生錯誤,err為捕捉的錯誤對象
require.EqualError(t, err, "failed to put to world state. failed inserting key")	// 産生的錯誤内容需要和預先定義的相同
// 傳回值相關斷言
require.Equal(t, []*chaincode.Asset{asset, asset}, assets)	// 傳回值需要和預先設定的值相同
require.Nil(t, assets)	// 傳回值需要為空
           

最後關于使用,其實隻需要用Go語言自帶的測試方法就可以了,即在鍊碼和測試檔案相同的目錄下運作如下指令:

go test
           

如果斷言全部正确,則列印如下内容:

$ go test

PASS

ok github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode 0.028s

否則會列印哪個斷言錯誤:

$ go test

— FAIL: TestGetAllAssets (0.00s)

smartcontract_test.go:203:

Error Trace: smartcontract_test.go:203

Error: An error is expected but got nil.

Test: TestGetAllAssets

FAIL

exit status 1

FAIL github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode 0.041s

3.為atcc鍊碼寫一個單元測試

atcc鍊碼見之前的部落格:

Fabric 2.0,編寫及使用鍊碼

今天看了下代碼,其實當時的atcc和asset-transfer-basic的各個方法一模一樣,應該當時就是照着這個寫的吧,那麼其實我們可以根據我們鍊碼的情況修改一下包名之類的部分,剩下的部分直接抄就行了。

這裡鍊碼的路徑情況如下:

├── assetsManager.go
└── atcc
    └──atcc.go
├── core.yaml
├── go.mod
├── go.sum
├── installChainCode.sh
└── vendor
    ├── github.com
    ├── golang.org
    ├── google.golang.org
    ├── gopkg.in
    └── modules.txt
           

首先我們需要把mock檔案夾複制到atcc.go所在的目錄,另外當時的子產品名為main,鍊碼的包名為atcc,那麼測試檔案的包名為atcc_test,引用鍊碼時直接使用main/atcc即可,在我這裡,順手把他重命名為chaincode以減少copy代碼之後的修改量。

搞定這些之後,使用如下指令建構一下依賴:

go mod tidy
go mod vendor
           

然後會把依賴自動添加到go.mod檔案中。

最後在atcc.go所在目錄atcc建立atcc_test.go,寫入如下内容:

package atcc_test

import (
	"encoding/json"
	"fmt"
	"testing"
	"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
	chaincode "main/atcc"
	"main/atcc/mocks"
	"github.com/stretchr/testify/require"
)

func TestInitLedger(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	assetTransfer := chaincode.SmartContract{}
	err := assetTransfer.InitLedger(transactionContext)
	require.NoError(t, err)

	chaincodeStub.PutStateReturns(fmt.Errorf("failed inserting key"))
	err = assetTransfer.InitLedger(transactionContext)
	require.EqualError(t, err, "failed to put to world state. failed inserting key")
}

func TestCreateAsset(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	assetTransfer := chaincode.SmartContract{}
	err := assetTransfer.CreateAsset(transactionContext, "", "", 0, "", 0)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns([]byte{}, nil)
	err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0)
	require.EqualError(t, err, "the asset asset1 already exists")

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0)
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}

func TestReadAsset(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	expectedAsset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(expectedAsset)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(bytes, nil)
	assetTransfer := chaincode.SmartContract{}
	asset, err := assetTransfer.ReadAsset(transactionContext, "")
	require.NoError(t, err)
	require.Equal(t, expectedAsset, asset)

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	_, err = assetTransfer.ReadAsset(transactionContext, "")
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")

	chaincodeStub.GetStateReturns(nil, nil)
	asset, err = assetTransfer.ReadAsset(transactionContext, "asset1")
	require.EqualError(t, err, "the asset asset1 does not exist")
	require.Nil(t, asset)
}

func TestAssetExists(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	assetTransfer := chaincode.SmartContract{}
	expectedAsset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(expectedAsset)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(bytes, nil)
	exist, err := assetTransfer.AssetExists(transactionContext, "asset1")
	require.NoError(t, err)
	require.Equal(t, true, exist)

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	_, err = assetTransfer.AssetExists(transactionContext, "asset1")
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}

func TestUpdateAsset(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	expectedAsset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(expectedAsset)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(bytes, nil)
	assetTransfer := chaincode.SmartContract{}
	err = assetTransfer.UpdateAsset(transactionContext, "", "", 0, "", 0)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(nil, nil)
	err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0)
	require.EqualError(t, err, "the asset asset1 does not exist")

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0)
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}

func TestDeleteAsset(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	asset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(asset)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(bytes, nil)
	chaincodeStub.DelStateReturns(nil)
	assetTransfer := chaincode.SmartContract{}
	err = assetTransfer.DeleteAsset(transactionContext, "")
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(nil, nil)
	err = assetTransfer.DeleteAsset(transactionContext, "asset1")
	require.EqualError(t, err, "the asset asset1 does not exist")

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	err = assetTransfer.DeleteAsset(transactionContext, "")
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}

func TestTransferAsset(t *testing.T) {
	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	asset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(asset)
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(bytes, nil)
	assetTransfer := chaincode.SmartContract{}
	err = assetTransfer.TransferAsset(transactionContext, "", "")
	require.NoError(t, err)

	chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset"))
	err = assetTransfer.TransferAsset(transactionContext, "", "")
	require.EqualError(t, err, "failed to read from world state: unable to retrieve asset")
}

func TestGetAllAssets(t *testing.T) {
	asset := &chaincode.Asset{ID: "asset1"}
	bytes, err := json.Marshal(asset)
	require.NoError(t, err)

	iterator := &mocks.StateQueryIterator{}
	iterator.HasNextReturnsOnCall(0, true)
	iterator.HasNextReturnsOnCall(1, true)
	iterator.HasNextReturnsOnCall(2, false)
	iterator.NextReturns(&queryresult.KV{Value: bytes}, nil)
	iterator.NextReturns(&queryresult.KV{Value: bytes}, nil)

	chaincodeStub := &mocks.ChaincodeStub{}
	transactionContext := &mocks.TransactionContext{}
	transactionContext.GetStubReturns(chaincodeStub)

	chaincodeStub.GetStateByRangeReturns(iterator, nil)
	assetTransfer := &chaincode.SmartContract{}
	assets, err := assetTransfer.GetAllAssets(transactionContext)
	require.NoError(t, err)
	require.Equal(t, []*chaincode.Asset{asset, asset}, assets)

	iterator.HasNextReturns(true)
	iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item"))
	assets, err = assetTransfer.GetAllAssets(transactionContext)
	require.EqualError(t, err, "failed retrieving next item")
	require.Nil(t, assets)

	chaincodeStub.GetStateByRangeReturns(nil, fmt.Errorf("failed retrieving all assets"))
	assets, err = assetTransfer.GetAllAssets(transactionContext)
	require.EqualError(t, err, "failed retrieving all assets")
	require.Nil(t, assets)
}

           

然後我們在atcc檔案夾下使用如下指令:

go test
           

可以看到如下輸出,說明單元測試均通過。

$ go test
PASS
ok  	main/atcc	0.038s
           

繼續閱讀