WHCSRL 技术网

Fabric 2.0,go链码单元测试

之前一直是完成链码的逻辑,然后打包部署在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
  • 1

这个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"
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.使用

2.1.概述

Mocks提供了链码中的GetState、PutState等方法的对应的返回值设置方法GetStateReturns和PutStateReturns方法等用于设置链码中调用GetState等方法的返回值及错误情况等,如果把链码类比为service层,那么这里我们可以把我们的返回值手动设置看作service层调用了我们自己实现的dao层,由此可以实现对service层的间接控制,所以到这里我们也可以看出来,其实链码测试时是无状态的,即调用存入的API之后并没有保存这个存入记录的状态,取不出对应的数据。

在使用之前,我们需要用一些代码来初始化返回值设置的桩:

chaincodeStub := &mocks.ChaincodeStub{}
transactionContext := &mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
  • 1
  • 2
  • 3

初始化好之后,我们就可以使用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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

那么相应的,我们可以调用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")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

从代码中可以看出,方法一里面我们手动设置了返回值为一个序列化的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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

然后我们可以对测试代码里出现的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)	// 第一个参数为迭代器,第二个参数为返回时产生的错误
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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)	// 返回值需要为空
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后关于使用,其实只需要用Go语言自带的测试方法就可以了,即在链码和测试文件相同的目录下运行如下命令:

go test
  • 1

如果断言全部正确,则打印如下内容:

$ 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

首先我们需要把mock文件夹复制到atcc.go所在的目录,另外当时的模块名为main,链码的包名为atcc,那么测试文件的包名为atcc_test,引用链码时直接使用main/atcc即可,在我这里,顺手把他重命名为chaincode以减少copy代码之后的修改量。

搞定这些之后,使用如下命令构建一下依赖:

go mod tidy
go mod vendor
  • 1
  • 2

然后会把依赖自动添加到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)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189

然后我们在atcc文件夹下使用如下命令:

go test
  • 1

可以看到如下输出,说明单元测试均通过。

$ go test
PASS
ok  	main/atcc	0.038s
  • 1
  • 2
  • 3
推荐阅读