In Fabric development, testing chaincode is a bit of a headache. First, in a real deployment, storage and queries in chaincode depend on the state database on the peer, so you cannot test it directly on your local machine. Second, chaincode runs inside a container, which makes it hard to get at the logs printed by your code.

Testing chaincode directly in the actual development environment is even more painful. Every round of debugging means restarting the whole network (possibly a multi-host deployment), creating and joining the channel, and installing and instantiating the chaincode, which badly hurts testing efficiency. Below are two ways to test chaincode. One is developer (dev) mode, which sets up a simple single-machine network locally for testing. The other is unit testing (UT), which tests all the interfaces automatically without starting any nodes.

Dev Mode

The Environment

To use the dev mode environment, first download fabric-samples and put it under $GOPATH/src. The dev mode directory is:

fabric-samples/chaincode-docker-devmode

First, a look at the docker-compose-simple.yaml file in that directory:
The network contains 1 orderer, 1 peer, 1 chaincode container (which runs the chaincode under test), and 1 cli container (which sends requests to test the chaincode).

Two things to note:

  • The command entry of the cli container shows that on startup it runs the script.sh script in the current directory. That script creates a channel named myc and joins the peer to it, so we only need to install and instantiate the chaincode.
  • The volumes of the chaincode container contain this mapping:

    - ./../chaincode:/opt/gopath/src/chaincode

    This means the fabric-samples/chaincode directory is mapped into the container, and that is where the chaincode under test needs to go. To keep things organized, we can give each chaincode its own directory under it and put the chaincode to test there. (You can also just change the mapping to point at the actual path of your own chaincode.)

Testing

Here I use the simplest example, sacc.go, a chaincode that only does basic storage (set) and query (get). The whole process uses three terminals:

Terminal 1: start the network
First go to the dev mode directory:

cd fabric-samples/chaincode-docker-devmode

Start the network:

docker-compose -f docker-compose-simple.yaml up

When you see Going to wait for newer blocks, startup has succeeded. There are now four containers in the network (1 orderer, 1 peer, 1 chaincode, 1 cli), and the channel myc has been created with the peer joined.

Terminal 2: build the chaincode
Enter the chaincode container:

docker exec -it chaincode bash

Build the chaincode you want to test:

cd sacc
go build

On success, the generated executable appears in the current directory. Now start it:

CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./sacc

Note: there is a small issue here I could not explain. The official tutorial uses port peer:7051, and the peer is indeed listening on 7051, but using 7051 fails with: Error starting SimpleAsset chaincode: error sending chaincode REGISTER.

When starting up ... appears, the chaincode has started. Terminal 2 shows the chaincode's logs (for example, anything printed with fmt.Print()).

Terminal 3: test the chaincode from cli
Enter the cli container:

docker exec -it cli bash

Install and instantiate the chaincode (instantiation sets the initial value of a to 10):

peer chaincode install -p chaincodedev/chaincode/sacc -n mycc -v 0
peer chaincode instantiate -n mycc -v 0 -c '{"Args":["a","10"]}' -C myc

Run the tests:
Call the set() interface to set a to 20:

peer chaincode invoke -n mycc -c '{"Args":["set", "a", "20"]}' -C myc

Call the get() interface to query a. The value has been updated to 20, and the test is done.

peer chaincode query -n mycc -c '{"Args":["get","a"]}' -C myc

Adding CouchDB to the Dev Environment

If the chaincode you are developing uses the rich queries provided by CouchDB, you need to add a CouchDB container to the test environment.

This only takes a few changes to docker-compose-simple.yaml:
First add a couchdb section to the file:

couchdb:
    container_name: couchdb
    image: hyperledger/fabric-couchdb
    environment:
      - COUCHDB_USER=
      - COUCHDB_PASSWORD=
    ports:
      - 5984:5984
    networks:
      - default

Add the following to the peer's environment section:

environment:
    - CORE_LEDGER_STATE_STATEDATABASE=CouchDB
    - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
    - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
    - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=

Add the following to the peer's depends_on section:

depends_on:
    - couchdb

After running the docker-compose command, the CouchDB container starts. You can also open http://localhost:5984/_utils in a browser to reach CouchDB's web admin UI, which shows the stored data more clearly and makes testing easier.

Unit Testing

Unit tests (UT) improve both debugging efficiency and code quality. Fabric provides a MockStub class for unit testing.

Unit Tests

Unit tests do not need any network nodes. With just our test file, we can call and test the chaincode's interfaces locally.

The idea is that MockStub keeps a map[string][]byte to simulate a key-value state database, so the PutStat() and GetStat() calls made by the chaincode actually operate on an in-memory map.

MockStub provides two main functions to simulate an endorsing peer calling the chaincode: MockInit() and MockInvoke(), which call the Init and Invoke interfaces respectively. Both take a string uuid (any value will do) and a two-dimensional byte array (the arguments supplied for the test).

Requirements for unit tests:

  • Import the testing package
  • Unit test files end in _test.go
  • Test case function names must start with Test

A unit test example
Below is a unit test for sacc.go. Since the code is simple, I put the tests for its interfaces into a single case. Create a sacc_test.go file with the following test case:

func TestFunc(t *testing.T) {
    cc := new(SimpleAsset)                          // create the Chaincode object
    stub := shim.NewMockStub("sacc", cc)            // create the MockStub object
    // call Init to set a to 90
    stub.MockInit("1", [][]byte{[]byte("a"), []byte("90")})
    // call get to query the value of a
    res := stub.MockInvoke("1", [][]byte{[]byte("get"), []byte("a")})
    fmt.Println("The value of a is", string(res.Payload))
    // call set to set a to 100
    stub.MockInvoke("1", [][]byte{[]byte("set"), []byte("a"), []byte("100")})
    // query the value of a again
    res = stub.MockInvoke("1", [][]byte{[]byte("get"), []byte("a")})
    fmt.Println("The new value of a is", string(res.Payload))
}

Run go test in the current directory. The output is:

You can also get more detailed results, such as coverage:

go test -cover -covermode count -coverprofile ./cover.out

In the output, coverage is 68.8%. The higher the coverage, the more complete the test cases.

Running the following command converts the cover.out file just generated into an HTML page, so you can see the coverage in more detail in the browser.

go tool cover -html=./cover.out

In real testing, each interface should have more than one case, covering negative cases and other edge conditions. You can also compare the expected result with the actual result in the test, and report an error when they differ so the case does not show PASS.

Benchmarks

Benchmark function names must start with Benchmark and take a parameter of type *testing.B. Here I treat one store plus one query as a single operation. The code:

func BenchmarkFunc(b *testing.B) {
    cc := new(SimpleAsset)
    stub := shim.NewMockStub("sacc", cc)
    for i :=0 ; i< b.N; i++ {
        stub.MockInvoke("1", [][]byte{[]byte("set"), []byte("a"), []byte("100")})
        stub.MockInvoke("1", [][]byte{[]byte("get"), []byte("a")})
    }
}

The loop runs b.N times. During a test the whole function is executed three times, with N increasing each time, e.g. 100, 10k, 300k.

Run the benchmark:

go test --benchmem -bench=.

The results are shown below. ns/op is the average number of nanoseconds per operation, and B/op is the average memory used per operation.

In a real deployment the chaincode's interfaces work against the state database, while here reads and writes are simulated in memory, so this benchmark does not mean much. If the chaincode contains some time-consuming computation, though, it is still worth benchmarking.

Summary

Testing with developer (dev) mode:

  • The upside is that the network is small, you can see the chaincode's logs directly in the terminal, and testing through the cli container is convenient (you can write test scripts, map them into the cli container, and run them automatically).
  • The downside is that every change to the chaincode still means restarting the whole network and building, installing, and instantiating the chaincode again, although all of that can be scripted into a single command.

Using unit tests:

  • The upside is that no network environment is needed: one simple command runs everything automatically on your local machine, and it helps you test interfaces thoroughly and systematically.
  • The downside is that rich queries based on CouchDB cannot be tested yet.

My blog will soon be synced to the Tencent Cloud+ Community, and you are welcome to join me there: https://cloud.tencent.com/developer/support-plan?invite_code=9yvch4ojkvbv

Tags: fabric

1 comment

  1. Anapodoton Anapodoton

    学到了

Leave a Comment