Debugging Fabric Chaincode -- Dev Mode and Unit Tests
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-devmodeFirst, 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
commandentry of the cli container shows that on startup it runs thescript.shscript in the current directory. That script creates a channel namedmycand joins the peer to it, so we only need to install and instantiate the chaincode. The
volumesof the chaincode container contain this mapping:- ./../chaincode:/opt/gopath/src/chaincodeThis means the
fabric-samples/chaincodedirectory 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-devmodeStart the network:
docker-compose -f docker-compose-simple.yaml upWhen 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 bashBuild the chaincode you want to test:
cd sacc
go buildOn success, the generated executable appears in the current directory. Now start it:
CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./saccNote: 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 bashInstall 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 mycRun the tests:
Call the set() interface to set a to 20:
peer chaincode invoke -n mycc -c '{"Args":["set", "a", "20"]}' -C mycCall 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 mycAdding 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:
- defaultAdd 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:
- couchdbAfter 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
testingpackage - 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.outIn 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
学到了