之前一直是完成鍊碼的邏輯,然後打包部署在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