阅读提示
通常情况下,我们完成链码的逻辑,将其打包部署在fabric网络后,才知道是否正确,但这样不断部署更新十分的浪费时间,同时不能及时测试并修改bug,对继续写代码也是一种阻碍。比较理想的开发方法是为开发的接口写好自动化测试,运行,出错,修改,直到通过测试用例,这也是TDD的思想,好处就是后面即使修改代码也可以很方便的完成回归测试,再配合git,就可以大胆开发了。
然而fabric环境中编写链码测试的其中一个难点在于上下文环境的模拟,关于这点,官方给出了一个单元测试的编写样例,如果是最新的fabric版本,可以在fabric-samples/asset-transfer-basic/chaincode-go/chaincode/
下找到这个smartcontract_test.go
单元测试示例文件。
在本文中可以学习如何对fabric智能合约中的方法进行单元测试,方便我们在不部署链码的条件下,能够高效的调试代码,下图为本文大致内容。
一、什么是单元测试
单元测试,又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作,一般对面向对象语言来说,这个最小单位是类或重要的类方法,它不仅可以用作功能测试,将单元测试集成到依赖集成工具之后,它们还可以在模块编译时执行,进行模块的回归测试。
建议使用TDD(测试驱动开发)思想来开发更加高效。
二、fabric单元测试需引入的必要依赖
测试文件首先要写包名和必要的依赖,首先包名与被测试的链码有关,必要引入的依赖如下:
package chaincode_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"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"
)
依赖概述
Mock提供了链码中账本操作的GetState、PutState等方法,以及对应的返回值设置方法GetStateReturns和PutStateReturns方法等,用于设置链码中调用GetState等方法的返回值及错误情况等,断言则是用require的方法来实现的。
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) // 第一个参数为迭代器,第二个参数为返回时产生的错误
断言相关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) // 返回值需要为空
三、fabric单元测试的实践
注意:链码测试时是无状态的,即调用写入API后并不会保存这个写入记录的状态,无法取出写入的数据。因此在使用之前,我们需要一些代码来作为初始化返回值设置。
chaincodeStub := &mocks.ChaincodeStub{}
transactionContext := &mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
初始化好后,我们就可以使用chaincodeStub来调用相应的返回值设置方法。
以AssetExists方法为例:
// 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)
//判断报错是否一致
}
从代码中可以看出,我们首先设置了返回值为一个序列化的json对象,链码返回值为true,之后,我们设置了返回值时产生的错误,因此方法执行后会获得我们事先设置的错误,判断测试结果由require方法的断言来实现。
以上为获取返回值(单个数据的方法),若想测试多个数据则比较麻烦,因为,链码中是通过范围查询返回迭代器,不停的next来输出所有数据,所以在写单元测试时,我们需要重写HasNext和Next返回值版本,相对复杂些。
首先,我们需要定义返回的迭代器,通过StateQueryIterator{}来创建,然后分别设置HasNext和Next的返回值,其中HasNext方法的方法的返回值通过HasNextReturnsOnCall(times,boolValue)来设置,其中为第几次调用HasNext,boolValue为调用的时候返回的布尔值(表示什么时候next结束)。Next方法的方法返回值通过NextReturns(result1 *queryresult.KV, result2 error)方法来设置,第一个参数为返回的具体值,第二个为Next方法抛出的错误,如果没有错误,设置为nil。具体代码如下:
func TestGetAllAssets(t *testing.T) {
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)
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)
}
四、总结
本文详细介绍了下fabric链码的单元测试API并举例说明。