之前一直是完成鏈碼的邏輯,然后打包部署在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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/321343.html
標籤:區塊鏈
下一篇:位元幣2
