Hyperledger fabric smart contract writing (4) unit testing

Reading tips

Normally, we complete the logic of the chain code and package it and deploy it on the fabric network before we know whether it is correct. However, it is a waste of time to continuously deploy updates. At the same time, we cannot test and modify bugs in time, and it is also a waste of time to continue writing code. hinder. The ideal development method is to write automated tests for the developed interface, run them, make errors, and modify them until the test cases are passed. This is also the idea of TDD. The advantage is that even if the code is modified later, regression testing can be easily completed, and then cooperate with git. , you can boldly develop it.
However, one of the difficulties in writing chain code tests in the fabric environment lies in the simulation of the context environment. Regarding this point, the official provides a unit test writing sample. If it is the latest fabric version, it can be found in fabric-samples/ Find this smartcontract_test.go unit test sample file under asset-transfer-basic/chaincode-go/chaincode/.

In this article, you can learn how to unit test the methods in the fabric smart contract, so that we can efficiently debug the code without deploying the chain code. The following figure shows the general content of this article.

1. What is unit testing

Unit testing, also called module testing, is a test for correctness testing of program modules (the smallest unit of software design). Generally, for object-oriented languages, this smallest unit is a class or an important class method. It not only They can be used as functional tests. After unit tests are integrated into dependency integration tools, they can also be executed when the module is compiled to perform regression testing of the module.

It is recommended to use TDD (Test Driven Development) idea to develop more efficiently.

2. Necessary dependencies that need to be introduced for fabric unit testing

The test file must first write the package name and necessary dependencies. First, the package name is related to the chain code being tested. The necessary dependencies are as follows:

package chaincode_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode"
"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode/mocks"
"github.com/stretchr/testify/require"
)

Dependency overview

Mock provides GetState, PutState and other methods for ledger operations in the chain code, as well as the corresponding return value setting methods GetStateReturns and PutStateReturns, etc., which are used to set the return value and error conditions of calling GetState and other methods in the chain code. Assertions are used require method to achieve.

Mock related API
//Get the stub object
chaincodeStub := & amp;mocks.ChaincodeStub{}
transactionContext := & amp;mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
//Set the return value of Get, Put and Del methods
chaincodeStub.GetStateReturns(bytes, nil) // The first parameter is the return value, the second parameter is the error
chaincodeStub.PutStateReturns(fmt.Errorf("failed inserting key")) // The parameter is an error
chaincodeStub.DelStateReturns(nil) //The parameter is wrong
// Create and set iterator objects
iterator := & amp;mocks.StateQueryIterator{}
iterator.HasNextReturnsOnCall(0, true) // The first parameter is the number of calls, and the second parameter is the corresponding return value. It can be used together with HasNextReturns or only one.
iterator.HasNextReturns(true) //Set the return value of the next HasNext method
iterator.NextReturns( & amp;queryresult.KV{Value: bytes}, nil) // The first parameter is the return value of the next call to Next, and the second parameter is the error generated when calling
//Set the return value of the GetStateByRange method.
chaincodeStub.GetStateByRangeReturns(iterator, nil) // The first parameter is the iterator, the second parameter is the error generated when returning

Assertion related API
// Error related assertions
require.NoError(t, err) // No error can be generated, err is the captured error object
require.EqualError(t, err, "failed to put to world state. failed inserting key") // The error content generated needs to be the same as the predefined one
//Return value related assertions
require.Equal(t, []*chaincode.Asset{asset, asset}, assets) //The return value needs to be the same as the preset value
require.Nil(t, assets) //The return value needs to be empty

3. Practice of fabric unit testing

Note: The chain code test is stateless, that is, after calling the write API, the state of the write record will not be saved, and the written data cannot be retrieved. Therefore, before use, we need some code to set the initialization return value.

chaincodeStub := & amp;mocks.ChaincodeStub{}
transactionContext := & amp;mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)

After initialization, we can use chaincodeStub to call the corresponding return value setting method.
Take the AssetExists method as an example:

// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}

return assetJSON != nil, nil
}

Correspondingly, we can call the GetStateReturns method to set its return value:

func TestAssetExists(t *testing.T) {
chaincodeStub := & amp;mocks.ChaincodeStub{}
transactionContext := & amp;mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
//Pre-deposit records in the ledger
assetTransfer := chaincode.SmartContract{}
expectedAsset := & amp;chaincode.Asset{ID: "asset1"}
bytes, err := json.Marshal(expectedAsset)
require.NoError(t, err)

//Judge whether the record results are consistent
chaincodeStub.GetStateReturns(bytes, nil)
exist, err := assetTransfer.AssetExists(transactionContext, "asset1")
require.NoError(t, err)
require.Equal(t, true, exist)
//Determine whether the error reports are consistent
}

As can be seen from the code, we first set the return value to a serialized json object, and the chain code return value is true. After that, we set the error generated when the return value is returned, so after the method is executed, we will get the value we set in advance. Error, the judgment test result is implemented by the assertion of the require method.

The above is the method of obtaining the return value (single data). If you want to test multiple data, it is more troublesome, because the chain code returns the iterator through range query, and keeps next to output all the data, so when writing unit tests , we need to rewrite the HasNext and Next return value versions, which is relatively complicated.

First, we need to define the returned iterator, create it through StateQueryIterator{}, and then set the return values of HasNext and Next respectively. The return value of the HasNext method is set through HasNextReturnsOnCall(times, boolValue), where is the number of times When calling HasNext, boolValue is the Boolean value returned when calling (indicating when next will end). The method return value of the Next method is set through the NextReturns(result1 *queryresult.KV, result2 error) method. The first parameter is the specific value returned, and the second parameter is the error thrown by the Next method. If there is no error, set it to nil. . The specific code is as follows:

func TestGetAllAssets(t *testing.T) {
asset := & amp;chaincode.Asset{ID: "asset1"}
bytes, err := json.Marshal(asset)
require.NoError(t, err)
//New iterator
iterator := & amp;mocks.StateQueryIterator{}
//Set the iterator to have two values, and the third time HasNext returns no more content,
iterator.HasNextReturnsOnCall(0, true)
iterator.HasNextReturnsOnCall(1, true)
iterator.HasNextReturnsOnCall(2, false)
//Set the return value for the first two times when there is a value
iterator.NextReturns( & amp;queryresult.KV{Value: bytes}, nil)
iterator.NextReturns( & amp;queryresult.KV{Value: bytes}, nil)
//Create a new stub
chaincodeStub := & amp;mocks.ChaincodeStub{}
transactionContext := & amp;mocks.TransactionContext{}
transactionContext.GetStubReturns(chaincodeStub)
//Set the return iterator
chaincodeStub.GetStateByRangeReturns(iterator, nil)
assetTransfer := & amp;chaincode.SmartContract{}
assets, err := assetTransfer.GetAllAssets(transactionContext)
require.NoError(t, err)
//Batch acquisition method, get two asset assets
require.Equal(t, []*chaincode.Asset{asset, asset}, assets)

iterator.HasNextReturns(true)
iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item"))
assets, err = assetTransfer.GetAllAssets(transactionContext)
require.EqualError(t, err, "failed retrieving next item")
require.Nil(t, assets)

chaincodeStub.GetStateByRangeReturns(nil, fmt.Errorf("failed retrieving all assets"))
assets, err = assetTransfer.GetAllAssets(transactionContext)
require.EqualError(t, err, "failed retrieving all assets")
require.Nil(t, assets)
}

4. Summary

This article introduces the unit testing API of fabric chain code in detail and gives examples.

Reference document: https://blog.csdn.net/zekdot/article/details/120812789