在本教程中,將學習測試驅動開發的概念,瞭解如何在 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 與區塊鏈網絡進行通訊。