内容


面向 Go 开发人员的链代码深入研究,第 2 部分

对使用 Go 为 Hyperledger Fabric v0.6 编写的区块链链代码进行单元测试

如何使用 shim MockStub 对链代码执行单元测试,而不需要将它部署到区块链网络中

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 面向 Go 开发人员的链代码深入研究,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:面向 Go 开发人员的链代码深入研究,第 2 部分

敬请期待该系列的后续内容。

在本教程中,将学习测试驱动开发的概念,了解如何在 Golang 中应用此方法为 Hyperledger Fabric v0.6 编写链代码。

通常,对链代码执行单元测试很麻烦,因为您需要先将链代码部署到 Docker 容器中的区块链网络中,以便访问底层区块链基础架构,比如账本、交易信息等。本教程将展示一个替代方法,通过此方法,您可以使用我的 CustomMockStub(它扩展了 shim 包中提供的 MockStub) 轻松对链代码执行单元测试。

本教程的示例还演示了如何在链代码中获得非确定性函数,以及如何对这些非确定性函数进行测试。

前提条件

我们将继续介绍本教程系列的 第 1 部分 中介绍的住房贷款申请用例。

  • 您应该已经熟悉第 1 部分中介绍的链代码的基本特征。
  • 还应已使用 Hyperledger Fabric 的 v0.6 分支设置了您的 go 链代码开发环境,如第 1 部分所述。

请参阅本教程底部的 “可下载资源” 来下载本教程中的所有代码示例,以及 CustomMockStub 实现。

链代码是什么?

链代码(也称为智慧合同)是一组使用编程语言(比如 Golang 或 Java)编写的业务规则/逻辑,它规定了区块链网络中的不同参与者如何相互交易。

测试驱动开发是什么?

测试驱动开发(或 TDD)是一种开发方法,要求开发人员在编写实际的实现代码之前 编写一个测试。测试驱动开发改变了您的关注点。无需考虑如何实现代码,只需考虑如何验证代码。

大体上讲,TDD 包含 3 个阶段,您将循环执行这些阶段,直到所有任务需求都得到满足:

  1. 红色:在缺少任何实现代码的情况下编写一个测试。运行此测试,该测试将以失败告终。
  2. 绿色:编写获得通过的测试所需的最少代码量。这一步中编写的代码通常不是最佳的,也没有可靠的功能。
  3. 重构:这里可以采用两种途径。如果第 2 步中编写的代码不需要任何重大重构,则返回到第 1 步并编写下一个测试。另一方面,如果第 2 步中编写的代码需要对结构、功能、性能等进行重构,则编写一个新测试来揭示代码中的缺陷,然后重构该代码来通过测试。

因为 TDD 采用了一种结构化方式将问题说明分解为测试形式的更小组成部分,所以带来了以下好处:

  • 代码井然有序、设计精良且容易测试
  • 已证实您的代码能按预期运行
  • 更快地获得开发反馈
  • 高质量代码

关于 Golang 测试库和 MockStub

本教程使用 Golang 提供的原生测试库来编写测试。可以使用包测试来对 Go 包执行自动化测试。测试包类似于测试运行器,可使用 go test 命令进行调用。

我们需要一种方式来为对链代码开发中广泛使用的 shim.ChaincodeStubInterface 的调用创建桩代码 (stub)。所幸,shim 包包含 MockStub 实现,在单元测试期间可使用它为实际链代码中的 ChaincodeStubInterface 创建桩代码。

尽管 MockStub 包含 Hyperledger Fabric v0.6 中的大部分常用函数的实现,但不幸的是,MockStub 没有实现其他一些方法,比如 ReadCertAttribute。因为大多数链代码都使用此方法根据交易证书检索属性来执行访问控制,所以能为此方法创建桩代码并对我们的链代码执行全面单元测试很重要。所以我编写了一个自定义 MockStub,它通过实现一些未实现的方法并将现有方法委托给 shim.MockStub 来扩展 shim.MockStub 功能。

清单 1. CustomMockStub 实现的代码段
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 方法中的属性图。我还重写了 MockInitMockQueryMockInvoke 方法,以便在调用链代码时传入我的 CustomMockStub。

入门

开始之前,请按照 IBM Bluemix 文档中的步骤从 “设置开发环境” 开始,确保完成链代码开发环境的设置。在到达题为 “设置开发管道” 的小节时,您已经为开始使用 Go 开发链代码做好了准备。

然后下载并解压本教程底部的 “可下载资源” 部分的源代码。复制 varunmockstub.go 文件并放在您设置的 Hyperledger 文件夹下的以下路径中:
$GOROOT/src/github.com/Hyperledger/fabric/core/chaincode/shim/

在本教程中,我们假设需要为一个贷款申请实现 CRUD 操作。

在 Golang 开发环境中创建一个 sample_tdd 文件夹,并在其中创建以下两个文件:

  1. sample_chaincode_test.go – 此文件表示将包含 sample_chaincode.go 的所有测试的测试套件。测试套件的文件名应具有以下格式:*_test.go。
  2. sample_chaincode.go – 住房贷款申请用例的实际实现代码。

我们现在开始设置 sample_chaincode_test.go 文件。清单 2 给出了其中的包和导入语句。

清单 2. sample_chaincode_test.go 中的包和导入语句
package main
import (
    "encoding/json"
    "fmt"
    "testing"
    "github.com/hyperledger/fabric/core/chaincode/shim"
)

在清单 2 中,第 5 行从 Go 导入测试包,第 6 行导入将用于编写链代码的 shim 包,其中还包含用于单元测试的 CustomMockStub 实现。

CreateLoanApplication 实现

我们采用测试驱动开发来实现 sample_chaincode.go 文件中的 CreateLoanApplication 方法。

要求

  1. CreateLoanApplication 方法应获取以下输入:一个贷款申请 ID、一个表示要创建的贷款申请的 JSON 字符串,以及 ChaincodeStubInterface,后者将用于与底层 Hyperledger Fabric 基础架构进行通信。
  2. 它应返回两个参数:表示所创建的贷款申请的序列化 JSON 字符串,以及一个 error 对象。
  3. 如果缺少输入/输入无效,则会抛出验证错误。
清单 3. 第一个测试的代码
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 文件:

清单 4. 为了通过测试而需要向 sample_chaincode.go 添加的最少代码
package main

再次运行测试。测试失败并抛出以下错误:

1 ./sample_chaincode_test.go:18: undefined: SampleChaincode

测试失败是因为,sample_chaincode.go 文件没有定义 SampleChaincode。

让我们将此代码添加到 sample_chaincode.go 文件中:

清单 5. 向 sample_chaincode.go 添加另一段代码
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:

清单 6. 向 sample_chaincode.go 添加另一段代码
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

清单 7. 将 CreateLoanApplication 方法添加到 sample_chaincode.go
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
    return nil, nil
}

添加以下测试,以确保从 CreateLoanApplication 方法返回了一个验证错误来响应空输入参数。

清单 8. 添加针对验证错误的测试
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 添加通过测试所需的最少量代码:

清单 9. 为了通过测试而需要向 sample_chaincode.js 添加的最少量代码
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 方法会始终返回一个错误。现在编写另一个测试,该测试将揭示此缺陷并导致代码重构。

清单 10. 一个揭示缺陷的新测试
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 代码,以便通过此测试。

清单 11. 重构 sample_chaincode.js 中的 CreateLoanApplication 代码
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

在我们的下一个测试中,需要验证贷款申请是否已实际创建并写入区块链。将以下测试添加到测试文件中:

清单 12. 验证贷款申请是否已创建并写入区块链中的测试
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 方法中,这段代码会将输入贷款申请存储到账本上。

清单 13. 将输入贷款申请存储到账本的代码
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 方法的单元测试和开发。

Invoke 方法实现

让我们使用测试驱动开发来实现 shim.Chaincode.Invoke 方法。Invoke 方法由链代码基础架构调用,它传入 ChaincodeStubInterface 的合适实例,以及链代码的调用方(客户应用程序)所传入的函数名和参数。

要求

  1. Invoke 方法应检查输入函数名称参数,并将执行任务委托给合适的处理函数。
  2. 如果输入函数名称是无效的,Invoke 方法会返回一个错误。
  3. Invoke 方法应基于链代码的调用方的交易证书,实现/委托访问控制和权限管理。应该只允许 Bank_Admin 调用 CreateLoanApplication 方法。

第一个测试将验证上面的 “要求 3” 中列出的功能。

清单 14. TestInvokeValidation 测试的代码段
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 方法中编写通过此测试所需的最少量代码。这是测试的绿色阶段。

清单 15. 为了通过测试而需要在 sample_chaincode.go 中的 Invoke 方法中包含的最少量代码
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 并期望测试通过。

清单 16. TestInvokeValidation2 的代码段
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 的代码。

清单 17. 重构 sample_chaincode.go 中的 Invoke 方法代码
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 测试将会通过。

清单 18. 测试要求 1 和要求 2 中列出的功能的代码
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 方法:

清单 19. 为了通过测试而需要向 sample_chaincode.go 中的 Invoke 方法添加的最少量代码
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 测试。

清单 20. TestInvokeFunctionValidation2 测试的代码
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 方法,以便处理函数调用委托。

清单 21. 重构 sample_chaincode.go 中的 Invoke 方法
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 方法。

清单 22. 重构 TestInvokeFunctionValidation2 测试
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 函数的一个非确定性版本。这意味着,如果使用相同输入多次执行此函数,将会得到不同的结果。

清单 23. CreateLoanApplication 函数的一个非确定性版本
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 展示了如何测试某个方法是否是非确定性的。

清单 24. 测试某个方法是否是非确定性的
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 方法实现 CreateLoanApplicationInvoke 方法,执行测试驱动的链代码开发。本教程演示了使用来自 go 的默认测试包编写单元测试和创建自定义模拟桩代码的步骤,其中扩展了 shim 包中的默认 MockStub 实现来满足您的测试需求。最后,您了解了一个函数如何变成不确定函数,如何在开发期间测试这种函数。

本系列的最后一篇教程将展示如何在 Node.js 中创建一个客户端应用程序,以便利用 Hyperledger Fabric 客户端 SDK 与区块链网络进行通信。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing, 物联网
ArticleID=1045945
ArticleTitle=面向 Go 开发人员的链代码深入研究,第 2 部分: 对使用 Go 为 Hyperledger Fabric v0.6 编写的区块链链代码进行单元测试
publish-date=05182017