目次


Go 開発者のためのチェーンコード、第 2 回

Go で作成した Hyperledger Fabric v0.6 用のブロックチェーン・チェーンコードの単体テストを実施する

チェーンコードをブロックチェーン・ネットワークにデプロイすることなく、shim の MockStub を使用してチェーンコードの単体テストを実施する方法

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Go 開発者のためのチェーンコード、第 2 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Go 開発者のためのチェーンコード、第 2 回

このシリーズの続きに乞うご期待。

このチュートリアルでは、テスト駆動型開発の概念を学び、この開発手法を、Golang で Hyperledger Fabric v0.6 のチェーンコードを作成するプロセスに適用する方法を見ていきます。

通常、チェーンコードの単体テストを行うには、レジャーやトランザクション情報などの基礎となるブロックチェーン・インフラストラクチャーにアクセスできるよう、チェーンコードを Docker コンテナーに格納した上でブロックチェーン・ネットワークにデプロイするという厄介なプロセスが伴います。このチュートリアルでは、このプロセスを省略してすぐにチェーンコードの単体テストを行う方法として、shim パッケージに含まれる MockStub を継承した CustomMockStub を使って単体テストを実施する方法を説明します。

また、チェーンコード内の関数が非確定的になってしまう理由を説明するサンプル・コード、そしてそのような非確定的な関数をテストする方法を説明するサンプル・コードも記載します。

前提条件

このチュートリアル・シリーズでは例として、第 1 回で紹介した住宅ローン申請の事例を引き続き使用します。

  • 第 1 回で取り上げた、チェーンコードの基本的要素を理解している必要があります。
  • 第 1 回で説明したように、Hyperledger Fabric の v0.6 ブランチを使用して Go チェーンコード開発環境がセットアップされている必要があります。

このチュートリアルに記載するすべてのサンプル・コードを CustomMockStub 実装と併せてダウンロードするには、チュートリアルの終わりに記載されている「ダウンロード可能なリソース」セクションを参照してください。

チェーンコードとは何か

スマート・コントラクトとも呼ばれるチェーンコードとは、ブロックチェーン・ネットワーク内で参加者間のトランザクションを制御するビジネス・ルール/ロジック一式のことです。チェーンコードは、Golang または Java などのプログラミング言語で作成されます。

テスト駆動型開発とは何か

テスト駆動型開発 (TDD) とは、開発者が実際の実装コードを作成する前に、テストを作成するという開発手法のことです。テスト駆動型開発では、開発者がフォーカスする対象が変わり、コードを実装する方法を考えるのではなく、コードの有効性を確認する方法を考えることになります。

大まかに言うと、TDD は以下の 3 つのフェーズからなり、すべてのタスクの要件が満たされるまで、これらのフェーズを周期的に繰り返します。

  1. : 実装コードがない状態でテストを作成します。つまり、失敗するテストを作成して実行するということです。
  2. : テストを合格させるために必要な最小限のコードを作成します。このフェーズで作成するコードは、通常は最適なものでも機能的に堅牢なものでもありません。
  3. リファクタリング: このフェーズでは、開発プロセスが 2 つに分岐する可能性があります。フェーズ 2 で作成したコードに大々的なリファクタリングが必要でなければ、フェーズ 1 に戻って次のテストを作成します。一方、フェーズ 2 で作成したコードを、構造、機能、パフォーマンスなどの点でリファクタリングしなければならない場合は、そのコードの欠陥を明らかにする新しいテストを作成した上で、欠陥のあるコードをリファクタリングしてテストを合格させます。

TDD では問題のステートメントを、テストという形の小さなチャンクに体系的に分割できることから、以下の有益な結果をもたらします。

  • うまく構成された、簡単にテストできるコード
  • コードが意図どおりに機能するという証拠
  • 迅速な開発フィードバック
  • 高品質のコード

Golang の testing ライブラリーと MockStub について

このチュートリアルでは、Golang がテストの作成をサポートするために提供しているネイティブ testing ライブラリーを使用します。Go パッケージの自動化テストに利用できるこの testing パッケージは、テスト・ランナーと似ていて、go test コマンドで呼び出すことができます。

単体テストでは、チェーンコード開発で広範囲にわたって使用されている shim.ChaincodeStubInterface の呼び出しをスタブ化する手段が必要となります。幸い、shim パッケージに含まれている MockStub 実装を使用すれば、単体テスト中に、実際のチェーンコード内の ChaincodeStubInterface をスタブ化することができます。

MockStub には Hyperledger Fabric v0.6 でとりわけよく使われている関数の実装が含まれていますが、あいにく、MockStub が実装していないメソッドもあります。その一例は、ReadCertAttribute です。ほとんどのチェーンコードでは、アクセスを制御するトランザクション証明書から属性を取得するためにこのメソッドを使用するので、完全な単体テストを行うには、このメソッドもスタブ化しなければなりません。そこで、私は shim.MockStub の機能を継承するカスタム MockStub を作成しました。CustomMockStub という名前のこのカスタム 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)
}

MockStub への参照を保持する CustomMockStub に、ReadCertAttribute メソッド内で使用する属性マップを指定します。また、チェーンコードの呼び出し中に CustomMockStub に渡すために、MockInitMockQuery、および MockInvoke メソッドもオーバーライドしました。

開始手順

手順を開始する前に、このリンク先の IBM Bluemix 資料で説明している開発環境のセットアップ手順に従ってください。「Setting up the development environment」セクションから「Set up your development pipeline」というタイトルのセクションにたどり着くまでの手順に従うことで、Go 言語でチェーンコードを開発する準備が整います。

次に、このチュートリアルの終わりにある「ダウンロード可能なリソース」セクションのリンクを使用してソース・コードをダウンロードし、圧縮解除します。ダウンロード・ファイルに含まれている varunmockstub.go ファイルをコピーして、このファイルを、セットアップした環境に含まれる Hyperledger フォルダー内の以下のパスに配置します。
$GOROOT/src/github.com/Hyperledger/fabric/core/chaincode/shim/

このチュートリアルで前提とする要件は、ローン申請用の CRUD 処理を実装することです。

Golang 開発環境内に sample_tdd という名前のフォルダーを作成し、そのフォルダー内に以下の 2 つのファイルを作成します。

  1. sample_chaincode_test.go – このファイルは、sample_chaincode.go を対象としたテストのすべてが含まれるテスト・スイートを表します。テスト・スイートのファイル名は、*_test.go という形式でなければなりません。
  2. sample_chaincode.go – 住宅ローン申請の事例に対して実装する実際のコードです。

まずは、sample_chaincode_test.go ファイルをセットアップしましょう。リスト 2 に、package および import ステートメントを記載します。

リスト 2. sample_chaincode_test.go 内の package および import ステートメント
package main
import (
    "encoding/json"
    "fmt"
    "testing"
    "github.com/hyperledger/fabric/core/chaincode/shim"
)

リスト 2 では、行 5 で Go から testing パッケージをインポートし、行 6 で shim パッケージをインポートします。チェーンコードを作成するために使用する shim パッケージには、単体テスト用の CustomMockStub 実装も含まれています。

CreateLoanApplication の実装

テスト駆動型開発手法に従って、sample_chaincode.go ファイル内に CreateLoanApplication メソッドを実装しましょう。

要件

  1. CreateLoanApplication メソッドは、ローン申請 ID と ChaincodeStubInterface を入力として取る必要があります。ローン申請 ID は、作成するローン申請を表す JSON 文字列です。ChaincodeStubInterface は、基礎となる Hyperledger Fabric インフラストラクチャーと対話するために使用します。
  2. このメソッドが返さなければならないパラメーターは 2 つあります。1 つは、作成されたローン申請を表す、直列化された JSON 文字列、もう 1 つはエラー・オブジェクトです。
  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 パッケージはテスト関数を識別して実行することができます。また、テスト関数が取る testing.T パラメーターにより、テストを作成するために使用できるヘルパー・メソッドへのアクセスが可能になります。

リスト 2 の上に記載した要件によると、CreateLoanApplication メソッドはパラメーターの 1 つとして ChaincodeStubInterface を取らなければなりません。実行時には、Hyperledger Fabric が Query/Invoke/Init メソッドに ChaincodeStubInterface の実際のインスタンスを渡すので、単体テスト用の ChaincodeStubInterface のモック・オブジェクトが必要になります。

リスト 3 の行 5 で、入力として名前、SampleChaincode オブジェクト (実装するつもりのオブジェクト)、属性マップを取る新しい CustomMockStub を作成します。ここで作成されるスタブが、前述のカスタム・モック・スタブです。

この時点で、作成したテストを実行します。それには、sample_chaincode_test.go ファイルが格納されているルート・フォルダーから 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 フィルは空の状態で、package ステートメントさえも含まれていないためです。これは、「赤」フェーズを表します。

このテストを合格させるために必要な最小限のコードを作成しましょう。以下の行を sample_chaincode.go ファイルに追加してください。

リスト 4. テストを合格させるために sample_chaincode.go に追加する必要最小限のコード
package main

テストを再度実行します。テストは以下のエラーで失敗するはずです。

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

CustomMockStub は、SampleChaincode が実装する Init、Query、および Invoke メソッドが shim.Chaincode タイプのインスタンスであると期待します。これが、テストが失敗する理由です。

したがって、以下のコードを sample_chaincode.go に追加する必要があります。

リスト 5. sample_chaincode.go にさらに追加する必要があるコード
type SampleChaincode struct {
}

Run the test again. It will fail with the following error:

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)

The test fails since the CustomMockStub expects the SampleChaincode to implement the Init, Query, and Invoke methods to be considered an instance of type shim.Chaincode.

Now add the following code to 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. sample_chaincode.go への CreateLoanApplication メソッドの追加
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 コンストラクターに渡されることになるこの 2 つの属性は、チェーンコードの呼び出し側のトランザクション証明書から取得できる属性をシミュレーションできるようにするためのものです。

行 13 では、stub.MockInvoke メソッドを使用して、チェーンコード・インフラストラクチャーが実行時に直接 shim.Chaincode.Invoke メソッドを呼び出す方法をシミュレーションします。

MockInvoke メソッドはトランザクション ID (実行時に Blockchain インフラストラクチャーによって生成されることになる 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")
}

Now run the test suite. The TestInvokeValidation test will pass.

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']")
}

次は、CreateLoanApplication メソッドが実際に呼び出されたかどうかを検証するように、TestInvokeFunctionValidation2 テストをリファクタリングします。理想的には、標準のモック・ライブラリーに含まれるスパイ・オブジェクトを使用してリファクタリングすることになりますが、単純にするために、このテストでは 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
}

元の CreateLoanApplication メソッドはローン申請 ID を入力の一部として渡しましたが、上記のメソッドはそれとは異なり、乱数ジェネレーターを使用して ID を生成し、その ID を末尾に付加したローン申請内容を渡します。ローン申請 ID を生成する方法は、行 4 と 5 に示されています。行 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 から 28 では、前に作成したスタブと、チェーンコードの関数の個々の実行から返されたローン申請 ID を使用して、対応するレジャーからローン申請を取得し、それらが同等であるかどうか比較します。

確定的関数の場合、これらのローン申請の間に違いはまったくないはずです。

go test を使用して、このテストを実行してください。期待どおり、TestNonDeterministicFunction テストは失敗します。

NonDeterministicFunction は乱数ジェネレーターを使用してローン申請 ID を生成するため、この関数を呼び出すたびに、異なるローン申請 ID が使用されることになります。したがって、ローン申請が最終的に個々のピアのレジャーに保存されると、ローン申請内容が異なることから、検証ピアの間でレジャーの状態に不整合が生じることになります。

まとめ

このチュートリアルでは、チェーンコードのテスト駆動型開発を実践する方法を説明するために、TDD 手法に従って CreateLoanApplication メソッドと Invoke メソッドを実装しました。その過程で、go のデフォルトの testing パッケージを使用して単体テストを作成する手順、そして shim パッケージに含まれるデフォルトの MockStub 実装を継承して、テストのニーズに応じたカスタム・モック・スタブを作成する手順を説明しました。そして最後に、関数が非確定的なものになってしまう原因と、そのような関数を開発中にテストする方法を説明しました。

このチュートリアル・シリーズの最終回では、Hyperledger Fabric Client SDK を利用してブロックチェーン・ネットワークと対話する、クライアント・アプリケーションを Node.js で作成する方法を紹介します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=1047989
ArticleTitle=Go 開発者のためのチェーンコード、第 2 回: Go で作成した Hyperledger Fabric v0.6 用のブロックチェーン・チェーンコードの単体テストを実施する
publish-date=07272017