The importance of Smart Contract quality cannot be underestimated. In this post, we’ll talk about how to get up and running with Smart Contract testing in Hyperledger Fabric (HLF). More specific, this post will cover writing tests for your HLF chaincode in Go, meant only for testing chaincode functionality itself, without requiring a HLF network to run this against.
Smart contract structure
If you have no idea on how a Smart Contract, or “chaincode” in HLF looks like, then take a look at the fabric-samples repository. The basic structure is that you have a SmartContract
struct which implements 2 methods from the provided Chaincode Interface — Init
and Invoke
. You can then specify your Smart Contract-methods in the following form.
func (s *SmartContract) createUser(APIstub shim.ChaincodeStubInterface, args []string) sc.Response
The APIstub
parameter is where all your interactions with the underlying blockchain technology are happening. This should make it easy to write our own tests against these type of methods if we supply our own parameter that implements the shim.ChaincodeStubInterface
. Below is a very basic implementation of this interface, where only some methods are implemented to a bare minimum to provide basic functionality during testing (which is only keeping state in this example).
type TestAPIStub struct {
data map[string][]byte
}
func (stub *TestAPIStub) GetArgs() [][]byte {
return nil
}
func (stub *TestAPIStub) GetStringArgs() []string {
return nil
}
func (stub *TestAPIStub) GetFunctionAndParameters() (string, []string) {
return "", nil
}
func (stub *TestAPIStub) GetArgsSlice() ([]byte, error) {
return nil, nil
}
func (stub *TestAPIStub) GetTxID() string {
return ""
}
func (stub *TestAPIStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response {
return pb.Response{}
}
func (stub *TestAPIStub) GetState(key string) ([]byte, error) {
return stub.data[key], nil
}
func (stub *TestAPIStub) PutState(key string, value []byte) error {
stub.data[key] = value
return nil
}
func (stub *TestAPIStub) DelState(key string) error {
delete(stub.data, key)
return nil
}
func (stub *TestAPIStub) GetStateByRange(startKey, endKey string) (shim.StateQueryIteratorInterface, error) {
return nil, nil
}
func (stub *TestAPIStub) GetStateByPartialCompositeKey(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) {
return nil, nil
}
func (stub *TestAPIStub) CreateCompositeKey(objectType string, attributes []string) (string, error) {
return "", nil
}
func (stub *TestAPIStub) SplitCompositeKey(compositeKey string) (string, []string, error) {
return "", nil, nil
}
func (stub *TestAPIStub) GetQueryResult(query string) (shim.StateQueryIteratorInterface, error) {
return nil, nil
}
func (stub *TestAPIStub) GetHistoryForKey(key string) (shim.HistoryQueryIteratorInterface, error) {
return nil, nil
}
func (stub *TestAPIStub) GetCreator() ([]byte, error) {
return nil, nil
}
func (stub *TestAPIStub) GetTransient() (map[string][]byte, error) {
return nil, nil
}
func (stub *TestAPIStub) GetBinding() ([]byte, error) {
return nil, nil
}
func (stub *TestAPIStub) GetSignedProposal() (*pb.SignedProposal, error) {
return nil, nil
}
func (stub *TestAPIStub) GetTxTimestamp() (*timestamp.Timestamp, error) { // --> here is the problem with vendor package
return nil, nil
}
func (stub *TestAPIStub) SetEvent(name string, payload []byte) error {
return nil
}
A wild gopher appears
If you implement your own struct that satifies the shim.ChaincodeStubInterface
like the one above, then you will encounter the following error from your compiler.
cannot use testStub (type *TestAPIStub) as type shim.ChaincodeStubInterface in argument to smartContract.createUser:
*TestAPIStub does not implement shim.ChaincodeStubInterface (wrong type for GetTxTimestamp method)
have GetTxTimestamp() (*"github.com/golang/protobuf/ptypes/timestamp".Timestamp, error)
want GetTxTimestamp() (*"github.com/hyperledger/fabric/vendor/github.com/golang/protobuf/ptypes/timestamp".Timestamp, error)
Because the shim.ChaincodeStubInterface
is defined inside the HLF library, and this library uses govendor
for dependency management, we encounter a problem with these dependencies: We cannot satisfy this dependency directly, since you are not allowed to point your own dependencies to vendor dependencies of other libraries. It took me a while to figure this out because I never worked with the govendor
tool before.
Trying my luck in the rocketchat of hyperledger, I was pointed to this youtube video, explaining how to handle building chaincode with different dependency types.
The solution comes down to copying your chaincode to the hyperledger fabric source code, resolving your dependencies, and running your tests there. I’ll admit, it does not look clean, but it works nonetheless. I’ve put the content, described in the youtube video in a bash script below, which I can run for running tests. I added the go test -v
as ‘extra’ step, since this was not included in the original script described in the video.
#!/bin/bash
# See https://www.youtube.com/watch?v=-mlUaJbFHcM
# You'll need to package your chaincode together with files from fabric itself.
# This is explained in the youtube video above.
# The reason for this packaging, is so you can use their interfaces (for testing for example)
BLOCKCHAIN_PATH=${PWD}
CHAINCODE_FOLDER=mycc
# 0. Make sure you run this script in this folder
# 1. Copy chaincode files to fabric package
cp -r ${PWD}/chaincode ${GOPATH}/src/github.com/hyperledger/fabric
# 2. Go to your copied CC and set GOPATH
cd ${GOPATH}/src/github.com/hyperledger/fabric/chaincode/mycc
# 3. initialize vendor
govendor init
# 4. fetch all packages NOT in fabric
govendor fetch github.com/satori/go.uuid
# 5. fetch all packages in fabric. This includes:
# - fabric ones such as "github.com/hyperledger/fabric/common/util"
# - non fabric ones vendored in fabric such as "github.com/golang/protobuf/proto"
govendor add +external
# EXTRA: RUN TESTS
go test -v
# 6. now copy the vendor folder back to the original chaincode directory
cp -r ${GOPATH}/src/github.com/hyperledger/fabric/chaincode/mycc ${BLOCKCHAIN_PATH}/chaincode
# 7. build it to make sure all dependencies are resolved
cd ${BLOCKCHAIN_PATH}/chaincode/mycc
go build
# 8. The above should build without issues and create the executable
rm mycc
Seriously?
I know… looks like a lot of fuzz for trying to setup simple tests. Does it have to be so complex? Well, no actually. It was only afterwards, I discovered that HLF has already its own mock-implementation of the shim.ChaincodeStubInterface
, called mockStub
.
This mockStub
does exactly what we need for Go-testing. As an example, we can write our tests like the one below.
func TestCreateNewUser(t *testing.T) {
// ARRANGE
smartContract := new(SmartContract)
mockStub := shim.NewMockStub("mockstub", smartContract)
// testStub := TestAPIStub{data: make(map[string][]byte)}
txId := "mockTxID"
internalId := uuid.NewV4().String()
firstName := "John"
args := []string{
internalId,
firstName,
"Wick",
"[email protected]",
}
// ACT
mockStub.MockTransactionStart(txId)
response := smartContract.createUser(mockStub, args)
mockStub.MockTransactionEnd(txId)
// ASSERT
if s := response.GetStatus(); s != 200 {
t.Errorf("the status is %d, instead of 200", s)
t.Errorf("message: %s", response.Message)
}
var user User
var found bool
json.Unmarshal(mockStub.State[internalId], &user)
// Now you can assert properties of the user object you stored.
// ...
}
Conclusion
Using the mockStub
implementation from HLF itself, you can fairly easy start with writing tests in Go for your chaincode logic. If you want more fine-grained control on testing abilities for your chaincode, then you can always choose to write your own implementation of the shim.ChaincodeStubInterface
like described above.
Last modified on 2017-09-05