在本教程中,将学习测试驱动开发的概念,了解如何在 Golang 中应用此方法为 Hyperledger Fabric v0.6 编写链代码。php
一般,对链代码执行单元测试很麻烦,由于您须要先将链代码部署到 Docker 容器中的区块链网络中,以便访问底层区块链基础架构,好比帐本、交易信息等。本教程将展现一个替代方法,经过此方法,您可使用个人 CustomMockStub
(它扩展了 shim 包中提供的 MockStub
) 轻松对链代码执行单元测试。html
本教程的示例还演示了如何在链代码中得到非肯定性函数,以及如何对这些非肯定性函数进行测试。git
咱们将继续介绍本教程系列的 第 1 部分 中介绍的住房贷款申请用例。github
请参阅本教程底部的 “可下载资源” 来下载本教程中的全部代码示例,以及 CustomMockStub 实现。golang
您能够得到 2GB 运行时和容器内存,配置最多 10 个云服务,以及得到免费的服务台支持。试用 Bluemix,开始使用免费的 区块链高安全性业务网络(公测)计划 构建和测试区块链网络。它使用了最新的 Hyperledger Fabric v1.0 架构。编程
进一步了解 区块链高安全性业务网络(公测)计划 和 Hyperledger Fabric v1.0 的优势。json
链代码(也称为智慧合同)是一组使用编程语言(好比 Golang 或 Java)编写的业务规则/逻辑,它规定了区块链网络中的不一样参与者如何相互交易。数组
测试驱动开发(或 TDD)是一种开发方法,要求开发人员在编写实际的实现代码以前 编写一个测试。测试驱动开发改变了您的关注点。无需考虑如何实现代码,只需考虑如何验证代码。安全
大致上讲,TDD 包含 3 个阶段,您将循环执行这些阶段,直到全部任务需求都获得知足:bash
由于 TDD 采用了一种结构化方式将问题说明分解为测试形式的更小组成部分,因此带来了如下好处:
借助 区块链开发人员中心 内的 developerWorks 教程、课程、博客和社区支持,提升您的开发技能。
本教程使用 Golang 提供的原生测试库来编写测试。可使用包测试来对 Go 包执行自动化测试。测试包相似于测试运行器,可以使用 go test
命令进行调用。
咱们须要一种方式来为对链代码开发中普遍使用的 shim.ChaincodeStubInterface
的调用建立桩代码 (stub)。所幸,shim 包包含 MockStub
实现,在单元测试期间可以使用它为实际链代码中的 ChaincodeStubInterface
建立桩代码。
尽管 MockStub 包含 Hyperledger Fabric v0.6 中的大部分经常使用函数的实现,但不幸的是,MockStub 没有实现其余一些方法,好比 ReadCertAttribute
。由于大多数链代码都使用此方法根据交易证书检索属性来执行访问控制,因此能为此方法建立桩代码并对咱们的链代码执行全面单元测试很重要。因此我编写了一个自定义 MockStub,它经过实现一些未实现的方法并将现有方法委托给 shim.MockStub 来扩展 shim.MockStub 功能。
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
|
package shim
import (
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/hyperledger/fabric/core/chaincode/shim/crypto/attr"
)
type CustomMockStub struct {
stub *MockStub
CertAttributes map[string][]byte
}
// Constructor to initialise the CustomMockStub
func NewCustomMockStub(name string, cc Chaincode, attributes map[string][]byte) *CustomMockStub {
s := new(CustomMockStub)
s.stub = NewMockStub(name, cc)
s.CertAttributes = attributes
return s
}
func (mock *CustomMockStub) ReadCertAttribute(attributeName string) ([]byte, error) {
return mock.CertAttributes[attributeName], nil
}
func (mock *CustomMockStub) GetState(key string) ([]byte, error) {
return mock.stub.GetState(key)
}
func (mock *CustomMockStub) GetTxID() string {
return mock.stub.GetTxID()
}
func (mock *CustomMockStub) MockInit(uuid string, function string, args []string) ([]byte, error) {
mock.stub.args = getBytes(function, args)
mock.MockTransactionStart(uuid)
bytes, err := mock.stub.cc.Init(mock, function, args)
mock.MockTransactionEnd(uuid)
return bytes, err
}
func (mock *CustomMockStub) MockInvoke(uuid string, function string, args []string) ([]byte, error) {
mock.stub.args = getBytes(function, args)
mock.MockTransactionStart(uuid)
bytes, err := mock.stub.cc.Invoke(mock, function, args)
mock.MockTransactionEnd(uuid)
return bytes, err
}
func (mock *CustomMockStub) MockQuery(function string, args []string) ([]byte, error) {
mock.stub.args = getBytes(function, args)
// no transaction needed for queries
bytes, err := mock.stub.cc.Query(mock, function, args)
return bytes, err
}
func (mock *CustomMockStub) PutState(key string, value []byte) error {
return mock.stub.PutState(key, value)
}
func (mock *CustomMockStub) MockTransactionStart(txid string) {
mock.stub.MockTransactionStart(txid)
}
func (mock *CustomMockStub) MockTransactionEnd(uuid string) {
mock.stub.MockTransactionEnd(uuid)
}
|
CustomMockStub
包含对 MockStub
的引用,并且有一个将用于 ReadCertAttribute
方法中的属性图。我还重写了MockInit
、MockQuery
和 MockInvoke
方法,以便在调用链代码时传入个人 CustomMockStub。
开始以前,请按照 IBM Bluemix 文档中的步骤从 “设置开发环境” 开始,确保完成链代码开发环境的设置。在到达题为 “设置开发管道” 的小节时,您已经为开始使用 Go 开发链代码作好了准备。
而后下载并解压本教程底部的 “可下载资源” 部分的源代码。复制 varunmockstub.go 文件并放在您设置的 Hyperledger 文件夹下的如下路径中:
$GOROOT/src/github.com/Hyperledger/fabric/core/chaincode/shim/
在本教程中,咱们假设须要为一个贷款申请实现 CRUD 操做。
在 Golang 开发环境中建立一个 sample_tdd 文件夹,并在其中建立如下两个文件:
咱们如今开始设置 sample_chaincode_test.go 文件。清单 2 给出了其中的包和导入语句。
1
2
3
4
5
6
7
|
package main
import (
"encoding/json"
"fmt"
"testing"
"github.com/hyperledger/fabric/core/chaincode/shim"
)
|
在清单 2 中,第 5 行从 Go 导入测试包,第 6 行导入将用于编写链代码的 shim 包,其中还包含用于单元测试的CustomMockStub
实现。
咱们采用测试驱动开发来实现 sample_chaincode.go 文件中的 CreateLoanApplication 方法。
CreateLoanApplication
方法应获取如下输入:一个贷款申请 ID、一个表示要建立的贷款申请的 JSON 字符串,以及 ChaincodeStubInterface
,后者将用于与底层 Hyperledger Fabric 基础架构进行通讯。
1
2
3
4
5
6
7
8
9
|
func TestCreateLoanApplication (t *testing.T) {
fmt.Println("Entering TestCreateLoanApplication")
attributes := make(map[string][]byte)
//Create a custom MockStub that internally uses shim.MockStub
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
}
|
如清单 3 所示,全部测试函数都以 “Test” 关键字开头,以便 Golang 测试包能够识别并运行这些函数。测试函数接受testing.T
参数,该参数将提供对可用于编写测试的帮助器方法的访问。
依据清单 2 中所示的要求,CreateLoanApplication
方法应接受 ChaincodeStubInterface
做为其参数。由于 Hyperledger Fabric 在运行时会将 ChaincodeStubInterface
的实际实例传入 Query/Invoke/Init
方法中,因此您须要模拟 ChaincodeStubInterface
来实现单元测试。
在清单 3 中,第 5 行建立了一个新的 CustomMockStub
,该函数接受名称、(您打算实现的)SampleChaincode
对象和一个属性图做为参数。这里建立的桩代码是 前面讨论过的 一段自定义模拟桩代码。
如今从包含 sample_chaincode_test.go 文件的 root 文件夹运行 go test
来执行此测试。您的输出应相似于:
1 bash-3.2$ go test
2 can't load package: package .:
3 sample_chaincode.go:1:1:1 expected 'package', found 'EOF'
|
和预期同样,测试失败了,由于 sample_chaincode.go 文件是空的,甚至连包语句都没有。这表示测试处于红色阶段。
如今咱们来编写经过此测试所需的最少许代码。将下面这行添加到 sample_chaincode.go 文件:
1
|
package main
|
再次运行测试。测试失败并抛出如下错误:
1 ./sample_chaincode_test.go:18: undefined: SampleChaincode
|
测试失败是由于,sample_chaincode.go 文件没有定义 SampleChaincode。
让咱们将此代码添加到 sample_chaincode.go 文件中:
1
2
|
type SampleChaincode struct {
}
|
再次运行测试。它仍将失败并抛出如下错误:
1 ./sample_chaincode_test.go:16: cannot use new (SampleChaincode)
2 (type *SampleChaincode) as type shim.Chaincode in argument to
3 shim.NewMockStub:
4 *SampleChaincode does not implement shim.Chaincode
5 (missing Init method)
|
测试失败是由于 CustomMockStub 要求 SampleChaincode 实现 Init、Query 和 Invoke 方法,而后才会将其视为shim.Chaincode
类型的实例。
如今将如下代码添加到 sample_chaincode.go:
1
2
3
4
5
6
7
8
9
10
11
|
func (t *SampleChaincode) Init(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
return nil, nil
}
func (t *SampleChaincode) Query(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
return nil, nil
}
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
return nil, nil
}
|
再次运行测试时,测试经过了。这是测试的绿色阶段。
1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 19:10:08 MockStub( mockStub &{} )
4 PASS
|
将 CreateLoanApplication
方法添加到 sample_chaincode.go
:
CreateLoanApplication
方法添加到 sample_chaincode.go
1
2
3
4
|
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
return nil, nil
}
|
添加如下测试,以确保从 CreateLoanApplication
方法返回了一个验证错误来响应空输入参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func TestCreateLoanApplicationValidation(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
_, err := CreateLoanApplication(stub, []string{})
if err == nil {
t.Fatalf("Expected CreateLoanApplication to return validation error")
}
stub.MockTransactionEnd("t123")
}
|
请注意 stub.MockTransactionStart(“t123”) 和 stub.MockTransactionStop(“t123”) 调用。由于写入帐本的任何信息都须要位于交易上下文中,因此测试必须在调用 CreateLoanApplication 方法以前启动交易,由于 CreateLoanApplication 方法会将贷款申请保存到帐本中。而后必须结束具备相同 ID 的交易,以代表交易完成。
使用 go test
运行测试。
1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 22:55:52 MockStub( mockStub &{} )
4 Entering CreateLoanApplication
5 --- FAIL: TestCreateLoanApplicationValidation (0.00s)
6 sample_chaincode_test.go:35: Expected CreateLoanApplication to
return validation error
7 FAIL
8 exit status 1
|
跟预期同样,测试失败了。如今向 sample_chaincode.js 添加经过测试所需的最少许代码:
1
2
3
4
|
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
return nil, errors.New(“Expected atleast two arguments for loan application creation”)
}
|
再次使用 go test
运行测试。
1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 23:02:52 MockStub( mockStub &{} )
4 Entering CreateLoanApplication
5 PASS
|
测试经过。这是测试的绿色阶段,由于 CreateLoanApplication 方法会始终返回一个错误。如今编写另外一个测试,该测试将揭示此缺陷并致使代码重构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
var loanApplicationID = "la1"
var loanApplication = `{"id":"` + loanApplicationID + `","propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
func TestCreateLoanApplicationValidation2(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation2")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
_, err := CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
if err != nil {
t.Fatalf("Expected CreateLoanApplication to succeed")
}
stub.MockTransactionEnd("t123")
}
|
第 1 和第 2 行将为贷款申请建立测试数据,这些数据被用做 CreateLoanApplication 方法的参数。
如今运行该测试。跟预期同样,测试将失败。
1 Entering TestCreateLoanApplicationValidation2
2 2017/02/22 23:09:01 MockStub( mockStub &{} )
3 Entering CreateLoanApplication
4 --- FAIL: TestCreateLoanApplicationValidation2 (0.00s)
5 sample_chaincode_test.go:55 Expected CreateLoanApplication to succeed
6 FAIL
7 exit status 1
|
如今,重构 sample_chaincode.js 中的 CreateLoanApplication 代码,以便经过此测试。
1
2
3
4
5
6
7
8
9
|
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
if len(args) < 2 {
fmt.Println("Invalid number of args")
return nil, errors.New("Expected atleast two arguments for loan application creation")
}
return nil, nil
}
|
再次运行测试。测试将会经过。
1 Entering TestCreateLoanApplicationValidation2
2 2017/03/06 12:07:34 MockStub( mockStub &{} )
3 Entering CreateLoanApplication
4 PASS
|
在咱们的下一个测试中,须要验证贷款申请是否已实际建立并写入区块链。将如下测试添加到测试文件中:
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
|
func TestCreateLoanApplicationValidation3(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation3")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
stub.MockTransactionEnd("t123")
var la LoanApplication
bytes, err := stub.GetState(loanApplicationID)
if err != nil {
t.Fatalf("Could not fetch loan application with ID " + loanApplicationID)
}
err = json.Unmarshal(bytes, &la)
if err != nil {
t.Fatalf("Could not unmarshal loan application with ID " + loanApplicationID)
}
var errors = []string{}
var loanApplicationInput LoanApplication
err = json.Unmarshal([]byte(loanApplication), &loanApplicationInput)
if la.ID != loanApplicationInput.ID {
errors = append(errors, "Loan Application ID does not match")
}
if la.PropertyId != loanApplicationInput.PropertyId {
errors = append(errors, "Loan Application PropertyId does not match")
}
if la.PersonalInfo.Firstname != loanApplicationInput.PersonalInfo.Firstname {
errors = append(errors, "Loan Application PersonalInfo.Firstname does not match")
}
//Can be extended for all fields
if len(errors) > 0 {
t.Fatalf("Mismatch between input and stored Loan Application")
for j := 0; j < len(errors); j++ {
fmt.Println(errors[j])
}
}
}
|
第 1-12 行在设置方面与以前的测试一致。在第 14 行,测试尝试检索贷款申请对象,该对象应该已在成功完成第 10 行中调用的 CreateLoanApplication 方法时建立。
stub.GetState(loanApplicationID) 检索与键对应的字节数组值,在本例中该键为来自帐本的贷款申请 ID。
在第 18 行,测试尝试将检索的字节数组分解为能够操做和读取的 LoanApplication 结构。
接下来,测试将检索的贷款申请与 CreateLoanApplication 方法的原始输入进行比较,以确保贷款申请和正确的值一块儿持久保存在帐本上。我提供了一些比较某些字段的测试。也能够扩展这些测试来包含其余字段。
备注:此测试跳过了输入模式验证,直接测试贷款申请在帐本上的成功持久化。理想状况下,CreateLoanApplication 方法中应包含某种形式的输入模式验证并进行测试,可是,为了确保本教程简洁且可管理,我跳过了这部份内容。
运行测试。跟预期同样,它将失败并抛出如下错误:
1 2017/03/06 18:34:38 MockStub mockStub Getting la1 ()
2 --- FAIL: TestCreateLoanApplicationValidation3 (0.00s)
3 sample_chaincode_test.go:82 Could not unmarshal loan application with ID la1
4 FAIL
5 exit status 1
|
如今,将如下代码添加到 CreateLoanApplication 方法中,这段代码会将输入贷款申请存储到帐本上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
if len(args) < 2 {
fmt.Println("Invalid number of args")
return nil, errors.New("Expected atleast two arguments for loan application creation")
}
var loanApplicationId = args[0]
var loanApplicationInput = args[1]
//TODO: Include schema validation here
err := stub.PutState(loanApplicationId, []byte(loanApplicationInput))
if err != nil {
fmt.Println("Could not save loan application to ledger", err)
return nil, err
}
fmt.Println("Successfully saved loan application")
return []byte(loanApplicationInput), nil
}
|
第 9 和第 10 行从参数中检索 loanApplicationId 和 loanApplicationInput JSON 字符串。以前已经提到过,随后将执行模式验证。
第 13 行使用 stub.PutState 方法将贷款申请存储为键/值对。在转换为字节数组后,贷款申请 ID 被存储为键,贷款申请 JSON 字符串被存储为值。
再次运行 TestCreateLoanApplicationValidation3 测试。测试将会经过。咱们已根据最初的要求,完成了 CreateLoanApplication 方法的单元测试和开发。
让咱们使用测试驱动开发来实现 shim.Chaincode.Invoke 方法。Invoke 方法由链代码基础架构调用,它传入 ChaincodeStubInterface 的合适实例,以及链代码的调用方(客户应用程序)所传入的函数名和参数。
Bank_Admin
调用 CreateLoanApplication
方法。第一个测试将验证上面的 “要求 3” 中列出的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func TestInvokeValidation(t *testing.T) {
fmt.Println("Entering TestInvokeValidation")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("client")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
if err == nil {
t.Fatalf("Expected unauthorized user error to be returned")
}
}
|
第 1 部分 中已经解释过,链代码的调用方的交易证书可能包含用户定义的属性。这些属性为在链代码中执行访问控制和权限发挥着关键做用。
第 5 和第 6 行添加用户名和角色属性,而后这些属性被传递给 CustomMockStub
构造函数。这些属性应有助于模拟可从链代码调用方的交易证书检索的属性。
第 13 行使用 stub.MockInvoke
方法模拟链代码基础架构在运行时应如何直接调用 shim.Chaincode.Invoke 方法。
MockInvoke
方法接受交易 ID(由区块链基础架构在运行时生成)、函数名和输入参数。
再次运行该测试套件。跟预期同样,TestInvokeValidation
测试将会失败。这是测试的红色阶段。
1 --- FAIL: TestInvokeValidation (0.00s)
2 sample_chaincode_test.go:158 Expected unauthorized user error to be returned
3 FAIL
4 exit status 1
|
如今,在 sample_chaincode.go 中的 Invoke 方法中编写经过此测试所需的最少许代码。这是测试的绿色阶段。
1
2
3
4
|
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
fmt.Println("Entering Invoke")
return nil, errors.New("unauthorized user")
}
|
如今运行该测试套件。TestInvokeValidation 测试将会经过。
1 Entering TestInvokeValidation
2 2017/03/06 23:22:27 MockStub( mockStub &{} )
3 Entering Invoke
4 PASS
|
下一个测试将传入正确的角色 Bank_Admin
并指望测试经过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func TestInvokeValidation2(t *testing.T) {
fmt.Println("Entering TestInvokeValidation")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
if err != nil {
t.Fatalf("Expected CreateLoanApplication to be invoked")
}
}
|
运行该测试套件。跟预期同样,TestInvokeValidation2 测试将会失败。要经过此测试,咱们如今必须重构 sample_chaincode.go 中的 Invoke 的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
fmt.Println("Entering Invoke")
ubytes, _ := stub.ReadCertAttribute("username")
rbytes, _ := stub.ReadCertAttribute("role")
username := string(ubytes)
role := string(rbytes)
if role != "Bank_Admin" {
return nil, errors.New("caller with " + username + " and role " + role + " does not have
access to invoke CreateLoanApplication")
}
return nil, nil
}
|
如今运行该测试套件。TestInvokeValidation2 测试将会经过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func TestInvokeFunctionValidation(t *testing.T) {
fmt.Println("Entering TestInvokeFunctionValidation")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "InvalidFunctionName", []string{})
if err == nil {
t.Fatalf("Expected invalid function name error")
}
}
|
第 14 行验证是否从 Invoke 返回了合适的错误消息。
运行 TestInvokeFunctionValidation
测试。跟预期同样,它将失败并抛出如下输出:
1 --- FAIL: TestInvokeFunctionValidation (0.00s)
2 sample_chaincode_test.go:117 Expected invalid function name error
3 FAIL
4 exit status 1
|
如今让咱们进入测试的绿色阶段,编写经过此测试所需的最少许代码。使用此代码段更新 sample_chaincode.go 中的 Invoke
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
fmt.Println("Entering Invoke")
ubytes, _ := stub.ReadCertAttribute("username")
rbytes, _ := stub.ReadCertAttribute("role")
username := string(ubytes)
role := string(rbytes)
if role != "Bank_Admin" {
return nil, errors.New("caller with " + username + " and role " + role + " does not have access to invoke CreateLoanApplication")
}
return nil, errors.New("Invalid function name")
}
|
再次运行 TestInvokeFunctionValidation
测试。测试将会经过,由于 Invoke 方法会跟预期同样返回错误。但跟以前讨论的同样,您须要在下一个测试后重构此代码。
下一个测试将会传入正确的函数名 CreateLoanApplication
并要求调用该函数。此代码段展现了 TestInvokeFunctionValidation2 测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func TestInvokeFunctionValidation2(t *testing.T) {
fmt.Println("Entering TestInvokeFunctionValidation2")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{})
if err != nil {
t.Fatalf("Expected CreateLoanApplication function to be invoked")
}
}
|
运行 TestInvokeFunctionValidation2
测试。跟预期同样,测试将会失败。
1 Entering TestInvokeFunctionValidation2
2 2017/03/06 20:50:12 MockStub( mockStub &{} )
3 Entering Invoke
4 --- FAIL: TestInvokeFunctionValidation2 (0.00s)
5 sample_chaincode_test.go:133 Expected CreateLoanApplication function to be
invoked
6 FAIL
|
如今重构 sample_chaincode.go 中的 Invoke 方法,以便处理函数调用委托。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
fmt.Println("Entering Invoke")
ubytes, _ := stub.ReadCertAttribute("username")
rbytes, _ := stub.ReadCertAttribute("role")
username := string(ubytes)
role := string(rbytes)
if role != "Bank_Admin" {
return nil, errors.New("caller with " + username + " and role " + role + " does not have access to invoke CreateLoanApplication")
}
if function == "CreateLoanApplication" {
return CreateLoanApplication(stub, args)
}
return nil, errors.New("Invalid function name. Valid functions ['CreateLoanApplication']")
}
|
如今重构 TestInvokeFunctionValidation2
测试,以便验证是否实际调用了 CreateLoanApplication 方法。理想状况下,应该使用一个 spy 对象来完成此操做,标准模拟库中提供了该对象,但为了简便起见,此测试将检查 Invoke 方法返回的输出来确保实际调用了 CreateLoanApplication 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func TestInvokeFunctionValidation2(t *testing.T) {
fmt.Println("Entering TestInvokeFunctionValidation2")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
bytes, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
if err != nil {
t.Fatalf("Expected CreateLoanApplication function to be invoked")
}
//A spy could have been used here to ensure CreateLoanApplication method actually got invoked.
var la LoanApplication
err = json.Unmarshal(bytes, &la)
if err != nil {
t.Fatalf("Expected valid loan application JSON string to be returned from CreateLoanApplication method")
}
}
|
如今再次运行该测试套件。TestInvokeFunctionValidation2 测试将会经过。
本教程系列的 第 1 部分 已详细介绍,链代码必须是肯定性的。下面将经过一个示例进行演示。以一个基于 4 对等节点 Hyperledger Fabric 的区块链网络为例,其中全部 4 个对等节点都是验证对等节点。这意味着只要有一个交易须要写入区块链中,全部 4 个对等节点都将在其本地帐本副本上独立执行交易。简言之,4 个对等节点中的每一个节点都将使用相同的输入独立执行同一个链代码函数,以便更新它们的本地帐本状态。经过这种方式,全部 4 个对等节点最终将具备相同的帐本状态。
所以,对等节点对链代码的全部 4 次执行都必须得到相同的结果,从而使它们最终得到相同的帐本状态。这被称为肯定性链代码。
清单 23 演示了 CreateLoanApplication
函数的一个非肯定性版本。这意味着,若是使用相同输入屡次执行此函数,将会获得不一样的结果。
CreateLoanApplication
函数的一个非肯定性版本
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
|
func NonDeterministicFunction(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering NonDeterministicFunction")
//Use random number generator to generate the ID
var random = rand.New(rand.NewSource(time.Now().UnixNano()))
var loanApplicationID = "la1" + strconv.Itoa(random.Intn(1000))
var loanApplication = args[0]
var la LoanApplication
err := json.Unmarshal([]byte(loanApplication), &la)
if err != nil {
fmt.Println("Could not unmarshal loan application", err)
return nil, err
}
la.ID = loanApplicationID
laBytes, err := json.Marshal(&la)
if err != nil {
fmt.Println("Could not marshal loan application", err)
return nil, err
}
err = stub.PutState(loanApplicationID, laBytes)
if err != nil {
fmt.Println("Could not save loan application to ledger", err)
return nil, err
}
fmt.Println("Successfully saved loan application")
return []byte(loanApplicationID), nil
}
|
不一样于传入贷款申请 ID 做为输入的原始 CreateLoanApplication
方法,上面的方法使用一个随机数生成器生成该 ID,并将它附加到传入的贷款申请内容中。第 4 和第 5 行演示了如何生成贷款申请 ID。第 19 行将更新后的贷款申请内容存储到帐本上。
清单 24 展现了如何测试某个方法是不是非肯定性的。
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
|
func TestNonDeterministicFunction(t *testing.T) {
fmt.Println("Entering TestNonDeterministicFunction")
attributes := make(map[string][]byte)
const peerSize = 4
var stubs [peerSize]*shim.CustomMockStub
var responses [peerSize][]byte
var loanApplicationCustom = `{"propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
//Simulate execution of the chaincode function by multiple peers on their local ledgers
for j := 0; j < peerSize; j++ {
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("tx" + string(j))
resp, err := NonDeterministicFunction(stub, []string{loanApplicationCustom})
if err != nil {
t.Fatalf("Could not execute NonDeterministicFunction ")
}
stub.MockTransactionEnd("tx" + string(j))
stubs[j] = stub
responses[j] = resp
}
for i := 0; i < peerSize; i++ {
if i < (peerSize - 1) {
la1Bytes, _ := stubs[i].GetState(string(responses[i]))
la2Bytes, _ := stubs[i+1].GetState(string(responses[i+1]))
la1 := string(la1Bytes)
la2 := string(la2Bytes)
if la1 != la2 {
//TODO: Compare individual values to find mismatch
t.Fatalf("Expected all loan applications to be identical. Non Deterministic chaincode error")
}
}
//All loan applications retrieved from each of the peer's ledger's match. Function is deterministic
}
}
|
第 4 行定义了咱们想模拟的验证对等节点数量。
第 6 行建立了与验证对等节点大小匹配的桩代码。每一个桩代码都将用于执行链代码函数,并更新其帐本状态。
第 9 到第 22 行使用了以前建立的桩代码,使用相同的输入参数来执行该链代码函数,以便模拟验证对等节点在实际场景中将如何执行链代码函数。
第 21 行存储对链代码函数的每次执行的响应。在本例中,调用的函数名为 NonDeterministicFunction
,它将返回存储在帐本上的贷款申请 ID。
第 25 到第 38 行使用以前建立的桩代码和链代码函数的单独执行所返回的贷款申请 ID,以便从各个帐本检索贷款申请并比较它们是否相同。
对于肯定性函数,这些贷款申请应该是相同的。
如今使用 go test
运行测试。跟预期同样,TestNonDeterministicFunction
测试将会失败。
由于 NonDeterministicFunction
使用随机数生成器来生成贷款申请 ID,因此对此函数的屡次调用将得到不一样的 ID。所以,在将贷款申请最终保存到各个对等帐本时,贷款申请内容将会有所不一样,并致使各个验证对等节点的帐本状态不一致。
您如今已经了解了如何经过使用 TDD 方法实现 CreateLoanApplication
和 Invoke
方法,执行测试驱动的链代码开发。本教程演示了使用来自 go 的默认测试包编写单元测试和建立自定义模拟桩代码的步骤,其中扩展了 shim 包中的默认 MockStub
实现来知足您的测试需求。最后,您了解了一个函数如何变成不肯定函数,如何在开发期间测试这种函数。
本系列的最后一篇教程将展现如何在 Node.js 中建立一个客户端应用程序,以便利用 Hyperledger Fabric 客户端 SDK 与区块链网络进行通讯。
本教程的示例还演示了如何在链代码中得到非肯定性函数,以及如何对这些非肯定性函数进行测试。
您能够得到 2GB 运行时和容器内存,配置最多 10 个云服务,以及得到免费的服务台支持。试用 Bluemix,开始使用免费的 区块链高安全性业务网络(公测)计划 构建和测试区块链网络。它使用了最新的 Hyperledger Fabric v1.0 架构。
进一步了解 区块链高安全性业务网络(公测)计划 和 Hyperledger Fabric v1.0 的优势。
测试驱动开发(或 TDD)是一种开发方法,要求开发人员在编写实际的实现代码以前 编写一个测试。测试驱动开发改变了您的关注点。无需考虑如何实现代码,只需考虑如何验证代码。
由于 TDD 采用了一种结构化方式将问题说明分解为测试形式的更小组成部分,因此带来了如下好处:
借助 区块链开发人员中心 内的 developerWorks 教程、课程、博客和社区支持,提升您的开发技能。
尽管 MockStub 包含 Hyperledger Fabric v0.6 中的大部分经常使用函数的实现,但不幸的是,MockStub 没有实现其余一些方法,好比 ReadCertAttribute
。由于大多数链代码都使用此方法根据交易证书检索属性来执行访问控制,因此能为此方法建立桩代码并对咱们的链代码执行全面单元测试很重要。因此我编写了一个自定义 MockStub,它经过实现一些未实现的方法并将现有方法委托给 shim.MockStub 来扩展 shim.MockStub 功能。
CustomMockStub
包含对 MockStub
的引用,并且有一个将用于 ReadCertAttribute
方法中的属性图。我还重写了MockInit
、MockQuery
和 MockInvoke
方法,以便在调用链代码时传入个人 CustomMockStub。package main
import (
"encoding/json"
"fmt"
"testing"
"github.com/hyperledger/fabric/core/chaincode/shim"
)
咱们采用测试驱动开发来实现 sample_chaincode.go 文件中的 CreateLoanApplication 方法。
CreateLoanApplication
方法应接受
ChaincodeStubInterface
做为其参数。由于 Hyperledger Fabric 在运行时会将
ChaincodeStubInterface
的实际实例传入
Query/Invoke/Init
方法中,因此您须要模拟
ChaincodeStubInterface
来实现单元测试。
3 sample_chaincode.go:1:1:1 expected 'package', found 'EOF'
清单 4. 为了经过测试而须要向 sample_chaincode.go 添加的最少代码
type SampleChaincode struct {
}
如今将如下代码添加到 sample_chaincode.go:
1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 19:10:08 MockStub( mockStub &{} )
4 PASS
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
return nil, nil
}
func TestCreateLoanApplicationValidation(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
_, err := CreateLoanApplication(stub, []string{})
if err == nil {
t.Fatalf("Expected CreateLoanApplication to return validation error")
}
stub.MockTransactionEnd("t123")
}
go test
运行测试。1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 23:02:52 MockStub( mockStub &{} )
4 Entering CreateLoanApplication
5 PASS
var loanApplicationID = "la1"
var loanApplication = `{"id":"` + loanApplicationID + `","propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
func TestCreateLoanApplicationValidation2(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation2")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
_, err := CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
if err != nil {
t.Fatalf("Expected CreateLoanApplication to succeed")
}
stub.MockTransactionEnd("t123")
}
1 Entering TestCreateLoanApplicationValidation2
2 2017/03/06 12:07:34 MockStub( mockStub &{} )
3 Entering CreateLoanApplication
4 PASS
func TestCreateLoanApplicationValidation3(t *testing.T) {
fmt.Println("Entering TestCreateLoanApplicationValidation3")
attributes := make(map[string][]byte)
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("t123")
CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
stub.MockTransactionEnd("t123")
var la LoanApplication
bytes, err := stub.GetState(loanApplicationID)
if err != nil {
t.Fatalf("Could not fetch loan application with ID " + loanApplicationID)
}
err = json.Unmarshal(bytes, &la)
if err != nil {
t.Fatalf("Could not unmarshal loan application with ID " + loanApplicationID)
}
var errors = []string{}
var loanApplicationInput LoanApplication
err = json.Unmarshal([]byte(loanApplication), &loanApplicationInput)
if la.ID != loanApplicationInput.ID {
errors = append(errors, "Loan Application ID does not match")
}
if la.PropertyId != loanApplicationInput.PropertyId {
errors = append(errors, "Loan Application PropertyId does not match")
}
if la.PersonalInfo.Firstname != loanApplicationInput.PersonalInfo.Firstname {
errors = append(errors, "Loan Application PersonalInfo.Firstname does not match")
}
//Can be extended for all fields
if len(errors) > 0 {
t.Fatalf("Mismatch between input and stored Loan Application")
for j := 0; j < len(errors); j++ {
fmt.Println(errors[j])
}
}
}
在第 18 行,测试尝试将检索的字节数组分解为能够操做和读取的 LoanApplication 结构。
运行测试。跟预期同样,它将失败并抛出如下错误:
1 2017/03/06 18:34:38 MockStub mockStub Getting la1 ()
2 --- FAIL: TestCreateLoanApplicationValidation3 (0.00s)
3 sample_chaincode_test.go:82 Could not unmarshal loan application with ID la1
4 FAIL
5 exit status 1
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
fmt.Println("Entering CreateLoanApplication")
if len(args) < 2 {
fmt.Println("Invalid number of args")
return nil, errors.New("Expected atleast two arguments for loan application creation")
}
var loanApplicationId = args[0]
var loanApplicationInput = args[1]
//TODO: Include schema validation here
err := stub.PutState(loanApplicationId, []byte(loanApplicationInput))
if err != nil {
fmt.Println("Could not save loan application to ledger", err)
return nil, err
}
fmt.Println("Successfully saved loan application")
return []byte(loanApplicationInput), nil
}
再次运行 TestCreateLoanApplicationValidation3 测试。测试将会经过。咱们已根据最初的要求,完成了 CreateLoanApplication 方法的单元测试和开发。
CustomMockStub
构造函数。这些属性应有助于模拟可从链代码调用方的交易证书检索的属性。
再次运行该测试套件。跟预期同样,TestInvokeValidation
测试将会失败。这是测试的红色阶段。
1 --- FAIL: TestInvokeValidation (0.00s)
2 sample_chaincode_test.go:158 Expected unauthorized user error to be returned
3 FAIL
4 exit status 1
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
fmt.Println("Entering Invoke")
return nil, errors.New("unauthorized user")
}
清单 16. TestInvokeValidation2 的代码段
清单 17. 重构 sample_chaincode.go 中的 Invoke 方法代码
access to invoke CreateLoanApplication")
}
return nil, nil
}
func TestInvokeFunctionValidation(t *testing.T) {
fmt.Println("Entering TestInvokeFunctionValidation")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "InvalidFunctionName", []string{})
if err == nil {
t.Fatalf("Expected invalid function name error")
}
}
TestInvokeFunctionValidation
测试。测试将会经过,由于 Invoke 方法会跟预期同样返回错误。但跟以前讨论的同样,您须要在下一个测试后重构此代码。func TestInvokeFunctionValidation2(t *testing.T) {
fmt.Println("Entering TestInvokeFunctionValidation2")
attributes := make(map[string][]byte)
attributes["username"] = []byte("vojha24")
attributes["role"] = []byte("Bank_Admin")
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
_, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{})
if err != nil {
t.Fatalf("Expected CreateLoanApplication function to be invoked")
}
}
清单 21. 重构 sample_chaincode.go 中的 Invoke 方法
清单 22. 重构 TestInvokeFunctionValidation2 测试
测试非肯定性函数
清单 23 演示了 CreateLoanApplication
函数的一个非肯定性版本。这意味着,若是使用相同输入屡次执行此函数,将会获得不一样的结果。
不一样于传入贷款申请 ID 做为输入的原始 CreateLoanApplication
方法,上面的方法使用一个随机数生成器生成该 ID,并将它附加到传入的贷款申请内容中。第 4 和第 5 行演示了如何生成贷款申请 ID。第 19 行将更新后的贷款申请内容存储到帐本上。
func TestNonDeterministicFunction(t *testing.T) {
fmt.Println("Entering TestNonDeterministicFunction")
attributes := make(map[string][]byte)
const peerSize = 4
var stubs [peerSize]*shim.CustomMockStub
var responses [peerSize][]byte
var loanApplicationCustom = `{"propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
//Simulate execution of the chaincode function by multiple peers on their local ledgers
for j := 0; j < peerSize; j++ {
stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
if stub == nil {
t.Fatalf("MockStub creation failed")
}
stub.MockTransactionStart("tx" + string(j))
resp, err := NonDeterministicFunction(stub, []string{loanApplicationCustom})
if err != nil {
t.Fatalf("Could not execute NonDeterministicFunction ")
}
stub.MockTransactionEnd("tx" + string(j))
stubs[j] = stub
responses[j] = resp
}
for i := 0; i < peerSize; i++ {
if i < (peerSize - 1) {
la1Bytes, _ := stubs[i].GetState(string(responses[i]))
la2Bytes, _ := stubs[i+1].GetState(string(responses[i+1]))
la1 := string(la1Bytes)
la2 := string(la2Bytes)
if la1 != la2 {
//TODO: Compare individual values to find mismatch
t.Fatalf("Expected all loan applications to be identical. Non Deterministic chaincode error")
}
}
//All loan applications retrieved from each of the peer's ledger's match. Function is deterministic
}
}
第 9 到第 22 行使用了以前建立的桩代码,使用相同的输入参数来执行该链代码函数,以便模拟验证对等节点在实际场景中将如何执行链代码函数。
对于肯定性函数,这些贷款申请应该是相同的。
结束语
本系列的最后一篇教程将展现如何在 Node.js 中建立一个客户端应用程序,以便利用 Hyperledger Fabric 客户端 SDK 与区块链网络进行通讯。