Hyperledger fabric智能合约编写(四)单元测试

阅读提示

通常情况下,我们完成链码的逻辑,将其打包部署在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并举例说明。

参考文档:https://blog.csdn.net/zekdot/article/details/120812789

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值