github.com/s7techlab/cckit@v0.10.5/testing/README.md (about) 1 # Test Driven Hyperledger Fabric Golang chaincode development 2 3 ## How to fast and continuously check smart contract logic : blockchain transactions, fired events and chaincode permissioning 4 5 Testing stage is a critical requirement for software quality assurance, doesn't matter is this 6 web application or a smart contract. Tests must be fast enough to run on every commit to repository. 7 [CCKit](https://github.com/s7techlab/cckit/), programming toolkit for developing and testing Hyperledger Fabric Golang 8 chaincodes, enhances the development experience with extended version of MockStub for chaincode testing. 9 10 ### Steps in chaincode development process 11 12 A smart contract defines the different states of a business object and governs the processes that move the object between 13 these different states. Smart contracts allows architects and smart contract developers to define the 14 business processes and structure of data that are shared across different organizations collaborating in a blockchain network. 15 16 The job of a smart contract developer is to take an existing business process and express it as a smart contract in a 17 programming language. Steps of chaincode development: 18 19 * Define chaincode model - schema for state entries, input payload and events 20 * Define chaincode interface 21 * Implement chaincode instantiate method 22 * Implement chaincode methods with business logic 23 * Create tests 24 25 Test driven development (TDD) or Behavioral Driven Development, possibly, single way to develop smart contracts. 26 27 ### Chaincode (smart contract) testing 28 29 Tests must ensure that chaincode works as expected: 30 31 * particular input payload leads to particular business object state change 32 * particular (invalid) input payload leads to validation or other errors 33 * particular object state allow subset of state transitions (state machine) 34 35 Any software testing (chaincode or web application for example) may either be a manual or an automated process. Manual 36 software testing is led by a team or individual who will manually operate a software product and ensure it behaves as expected. 37 In case of chaincode tests you can manually invoke chaincode via `peer` cli tools. 38 39 Automated software testing is the practice of instrumenting input and output correctness checks for individual units of code. 40 During automated testing, code are executed in a test environment with simulated input. 41 42 ### Running chaincode 43 44 Deploying chaincode to blockchain network isn't the quickest thing in the world, there's a lot of time that can be saved 45 with testing. Also, more importantly, since blockchain is immutable and supposed to be secure because the code is on the 46 network, we rather not leave flaws in our code. 47 48 During chaincode development and deploying to live network we can divide testing to multiple stage - fast stage, when 49 testing only smart contract logic, and more complicated stage, when we do integration testing with live blockchain network, 50 multiple peers, deployed on-chain code (smart contracts) and off-chain application, that uses SDK to connect with 51 blockchain network peers. 52 53 ### Chaincode DEV mode 54 55 Deploying a Hyperledger Fabric blockchain network, chaincode installing and initializing, is quite complicated to set up and 56 a long procedure. Time to re-install / upgrade the code of a smart contract can be reduced by using 57 [chaincode dev mode](https://hyperledger-fabric.readthedocs.io/en/latest/peer-chaincode-devmode.html). Normally chaincodes 58 are started and maintained by peer. In “dev” mode, chaincode is built and started by the user. 59 This mode is useful during chaincode development phase for rapid code/build/run/debug cycle turnaround. However, the process 60 of updating the code will still be slow. 61 62 ### MockStub - mocked chaincode stub 63 64 Mocking is a unit testing phenomenon which helps to test objects in isolation by replacing dependent objects with 65 complex behavior with test objects with pre-defined/simulated behavior. These test objects are called as Mock objects. 66 67 The [shim](https://github.com/hyperledger/fabric/tree/master/core/chaincode/shim) package contains a 68 [MockStub](https://github.com/hyperledger/fabric/tree/master/core/chaincode/shim/mockstub.go) implementation 69 that wraps calls to a chaincode, simulating its behavior in the HLF peer environment. MockStub does not need to start 70 multiple docker containers with peer, world state database, chaincodes and allows to get test results almost immediately. 71 MockStub essentially replaces the SDK and peer environment and allows to test chaincode without actually starting your 72 blockchain network. It implements almost every function the actual stub does, but in memory. 73 74 ![mockstub](../docs/img/mockstub-hlf-peer.png) 75 76 `MockStub` from [https://github.com/hyperledger/fabric/](https://github.com/hyperledger/fabric/) repository includes 77 implementation for most of `shim.ChaincodeStubInterface` function, but until current version 78 of Hyperledger Fabric (1.4), the `MockStub` has not implemented some of the important methods such 79 as `GetCreator` or method for work with private state range, for example. Since chaincode would use `GetCreator` method 80 to get transaction creator certificate for access control, it's critical to be able to stub this method in order 81 to completely unit-test chaincode. 82 83 ### CCKit MockStub 84 85 CCKit [testing](.) package contains: 86 87 * [MockStub](mockstub.go) with implemented `GetTransient` and others methods and event subscription feature 88 * Test [identity](identity.go) creation helpers 89 * Chaincode response [expect](expect) helpers 90 91 92 # Example: `Commercial Paper` chaincode 93 94 ## Scenario 95 96 Official hyperledger fabric documentation contain detailed 97 [chaincode example](https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/scenario.html) - `Commercial Paper` 98 smart contract that defines the valid states for commercial paper, and the transaction logic that transition 99 a paper from one state to another. We will test [commercial paper extended chaincode example](../examples/cpaper_extended) 100 based on CCKit with protobuf state, described in this 101 [article](https://medium.com/coinmonks/hyperledger-fabric-smart-contract-data-model-protobuf-to-chaincode-state-mapping-191cdcfa0b78). 102 103 We can represent the lifecycle of a commercial paper using a state transition diagram: commercial papers transition 104 between issued, trading and redeemed states by means of the issue, buy and redeem transactions. 105 106 ![state](../examples/cpaper/img/state.png) 107 108 ## Requirements 109 110 To produce tests first we need to define requirements to tested application. Let’s start by listing our requirements 111 for commercial paper chaincode: 112 113 * It should allow the issuer to issue commercial paper 114 * It should allow the participant to buy commercial paper 115 * It should allow the owner to redeem commercial paper 116 117 Chaincode interface functions described in file [chaincode.go](../examples/cpaper_extended/chaincode.go), so we can see 118 all possible operations (transactions) with chaincode data: 119 120 ```go 121 Query("list", queryCPapers). 122 123 // Get method has 2 params - commercial paper primary key components 124 Query("get", queryCPaper, defparam.Proto(&schema.CommercialPaperId{})). 125 Query("getByExternalId", queryCPaperGetByExternalId, param.String("externalId")). 126 127 // txn methods 128 Invoke("issue", invokeCPaperIssue, defparam.Proto(&schema.IssueCommercialPaper{})). 129 Invoke("buy", invokeCPaperBuy, defparam.Proto(&schema.BuyCommercialPaper{})). 130 Invoke("redeem", invokeCPaperRedeem, defparam.Proto(&schema.RedeemCommercialPaper{})). 131 Invoke("delete", invokeCPaperDelete, defparam.Proto(&schema.CommercialPaperId{})) 132 133 ``` 134 135 ## Getting started 136 137 Before you begin, be sure to get `CCKit`: 138 139 `git clone git@github.com:s7techlab/cckit.git` 140 141 This will fetch and install the CCKit package with [examples](../examples). After that we need to install the dependencies 142 using command: 143 144 `go mod vendor` 145 146 ## Creating test suite 147 148 ### Testing in Go 149 150 Go has a built-in testing command called `go test` and a package `testing` which gives a minimal but complete 151 testing experience. In our example we use [Ginkgo](https://github.com/onsi/ginkgo) - BDD-style Go testing framework, 152 built on Go’s testing package, and allows to write readable tests in an efficient manner. It is best paired with 153 the [Gomega](https://github.com/onsi/gomega) matcher library, but is designed to be matcher-agnostic. 154 155 As with popular BDD frameworks in other languages, `Ginkgo` allows you to group tests in `Describe` and `Context` container blocks. 156 `Ginkgo` provides the `It` and `Specify` blocks which can hold your assertions. It also comes with handy structural utilities 157 such as `BeforeSuite`, `AfterSuite`, etc that allows you to separate test configuration from test creation, and improve code reuse. 158 159 `Ginkgo` also comes with support for writing asynchronous tests. This makes testing code that use channels with chaincode events 160 as easy as testing synchronous code. 161 162 ### Test package 163 164 To write a new test suite, create a file whose name ends _test.go that contains the TestXxx functions, in our case will 165 be [cpaper_extended/chaincode_test.go](../examples/cpaper_extended/chaincode_test.go) 166 167 Using separate package with tests [cpaper_extended_test](../examples/cpaper_extended/chaincode_test.go) instead of 168 [cpaper_extended](../examples/cpaper_extended/chaincode.go) allows us to respect 169 the encapsulation of the chaincode package: your tests will need to import chaincode and access it from the outside. 170 You cannot fiddle around with the internals, instead you focus on the exposed chaincode interface. 171 172 ### Import matchers and helpers 173 174 To get started, we need to import the `matcher` functionality from the Ginkgo testing package 175 so we can use different comparison mechanisms like comparing response objects or status codes. 176 177 We import the `ginkgo` and `gomega` packages with the `.` namespace, so that we can use functions from these packages 178 without the package prefix. This allows us to use `Describe` instead of `ginkgo.Describe`, and `Equal` instead 179 of `gomega.Equal`. 180 181 182 ### Bootstrap 183 184 The call to `RegisterFailHandler` registers a handler, the `Fail` function from the `Ginkgo` package. This creates the 185 coupling between `Ginkgo` and `Gomega`. 186 187 Test suite bootstrap example: 188 ```go 189 package main 190 191 import ( 192 "fmt" 193 "testing" 194 "github.com/s7techlab/cckit/examples/insurance/app" 195 . "github.com/onsi/ginkgo" 196 . "github.com/onsi/gomega" 197 testcc "github.com/s7techlab/cckit/testing" 198 expectcc "github.com/s7techlab/cckit/testing/expect" 199 ) 200 201 func TestCommercialPaper(t *testing.T) { 202 RegisterFailHandler(Fail) 203 RunSpecs(t, "Commercial paper suite") 204 } 205 206 var _ = Describe(`Commercial paper`, func() { 207 208 209 } 210 ``` 211 212 ### Test structure 213 This particular test specification can be written using `Ginkgo` as follows: 214 215 ```go 216 var _ = Describe(`CommercialPaper`, func() { 217 218 Describe("Commercial Paper lifecycle", func() { 219 220 It("Allow issuer to issue new commercial paper", func() { ... } 221 222 It("Allow issuer to get commercial paper by composite primary key", func() { ... } 223 224 It("Allow issuer to get commercial paper by unique key", func() { ... } 225 226 It("Allow issuer to get a list of commercial papers", func() { ... } 227 228 It("Allow buyer to buy commercial paper", func() { ... } 229 230 It("Allow buyer to redeem commercial paper", func() { ... } 231 232 It("Allow issuer to delete commercial paper", func() { ... } 233 } 234 } 235 ``` 236 237 ### Implementing tests 238 239 Now we go in depth to see how to create test functions specifically for chaincode development using `MockStub` features. 240 241 #### Creating chaincode instance 242 243 Tests suite usually starts with creating a new instance of chaincode, or we can also instantiate a new chaincode 244 instance before every test spec. This depends on how and what we want to test. In this example we instantiate a global 245 `commercial paper` chaincode that can be used in multiple test specs. 246 247 ```go 248 paperChaincode := testcc.NewMockStub( 249 // chaincode name 250 `commercial_paper`, 251 // chaincode implementation, supports Chaincode interface with Init and Invoke methods 252 cpaper_extended.NewCC(), 253 ) 254 ``` 255 256 #### Test chaincode `Init` method 257 258 All chaincode invocation (via SDK to blockchain peer or to MockStub) resulted as 259 [peer.Response](https://github.com/hyperledger/fabric/blob/release-1.4/protos/peer/proposal_response.pb.go) structure: 260 261 ```go 262 type Response struct { 263 // A status code that should follow the HTTP status codes. 264 Status int32 265 // A message associated with the response code. 266 Message string 267 // A payload that can be used to include metadata with this response. 268 Payload []byte 269 } 270 ``` 271 272 During tests we can check `Response` attribute: 273 274 * Status (error or success) 275 * Message string (contains error description) 276 * Payload contents (marshaled JSON or Protobuf) 277 278 `Testing` package [contains](expect/matcher.go) multiple helpers / wrappers on ginkgo `expect` functions. 279 280 Most frequently used helpers are: 281 282 * `ResponseOk` (*response* **peer.Response**) expects that peer response contains `ok` status code(`200`). Optionally you can pass either a `string` or an `OmegaMatcher` [any available matcher provided by Gomega](http://onsi.github.io/gomega/#provided-matchers), to check, for example, if the error contains a specific substring. 283 * `ResponseError` (*response* **peer.Response**) expects that peer response contains error status code (`500`). Optionally you can pass either a `string`, an `error` or an `OmegaMatcher` as for `ResponseOk` 284 * `PayloadIs`(*response* **peer.Response**, *target* **interface{}) expects that peer response contains `ok` status code (`200`) 285 and converts response to **target** type using `CCKit` [convert](../convert) package 286 287 For example we can simply test that `Init` method (invoked when the chaincode is initialised) returns successful status code: 288 289 ```go 290 BeforeSuite(func() { 291 // Init chaincode with admin identity 292 293 adminIdentity, err := testcc.IdentityFromFile(MspName, `testdata/admin.pem`, ioutil.ReadFile) 294 Expect(err).NotTo(HaveOccurred()) 295 296 expectcc.ResponseOk( 297 paperChaincode. 298 From(adminIdentity). 299 Init()) 300 }) 301 ```` 302 303 ## Test the `Issue` method 304 305 We expect that invocation of `issue` chaincode method will result in: 306 * response with `Ok` status 307 * event `IssueCommercialPaper` is fired 308 309 In the test we can invoke `issue` method via MockStub, check response status and check chaincode event. Chaincode events 310 can be receive from `chaincodeEventsChannel`. The `BeEquivalentTo` method of the `expect` functionality 311 comes in handy to compare the event payload. 312 313 ```go 314 It("Allow issuer to issue new commercial paper", func(done Done) { 315 //input payload for chaincode method 316 issueTransactionData := &schema.IssueCommercialPaper{ 317 Issuer: IssuerName, 318 PaperNumber: "0001", 319 IssueDate: ptypes.TimestampNow(), 320 MaturityDate: testcc.MustProtoTimestamp(time.Now().AddDate(0, 2, 0)), 321 FaceValue: 100000, 322 ExternalId: "EXT0001", 323 } 324 325 // we expect tha `issue` method invocation with particular input payload returns response with 200 code 326 // &schema.IssueCommercialPaper wil automatically converts to bytes via proto.Marshall function 327 expectcc.ResponseOk( 328 paperChaincode.Invoke(`issue`, issueTransactionData)) 329 330 // Validate event has been emitted with the transaction data 331 Expect(<-paperChaincode.ChaincodeEventsChannel).To(BeEquivalentTo(&peer.ChaincodeEvent{ 332 EventName: `IssueCommercialPaper`, 333 Payload: testcc.MustProtoMarshal(issueTransactionData), 334 })) 335 336 // Clear events channel after a test case that emits an event 337 paperChaincode.ClearEvents() 338 close(done) 339 }, 0.1) 340 ``` 341 342 This test will block until a response is received over the channel `paperChaincode.ChaincodeEventsChannel` (chaincode event). 343 A deadlock or timeout is a common failure mode for tests like this. A common pattern in such situations is to add a select 344 statement at the bottom of the function and include a <-time.After(X) channel to specify a timeout. Ginkgo has this pattern 345 built in. The body functions in all non-container blocks (`It` , `BeforeEache` etc ) can take an optional done `Done` argument. 346 347 Done is a `chan interface{}`. When `Ginkgo` detects that the `done Done` argument has been requested it runs the body function 348 as a goroutine, wrapping it with the necessary logic to apply a timeout assertion. You must either close the done channel, 349 or send something (anything) to it to tell `Ginkgo` that your test has ended. If your test doesn’t end after a timeout period, 350 `Ginkgo` will fail the test and move on the next one. 351 352 The default timeout is 1 second. You can modify this timeout by passing a float64 (in seconds) after the body function. 353 In this example we set the timeout to 0.1 second. 354 355 ## Test the `Get` method 356 357 We expect that invocation of `get` chaincode method will result in: 358 * response with `Ok` status 359 * response payload is marshaled `*schema.CommercialPaper` with certain attributes values 360 361 `PayloadIs` allows to check response status and converts to `*schema.CommercialPaper`, then `Expect` helps to check 362 equality of received data with expected values: 363 364 ```go 365 It("Allow issuer to get commercial paper by composite primary key", func() { 366 queryResponse := paperChaincode.Query("get", &schema.CommercialPaperId{ 367 Issuer: IssuerName, 368 PaperNumber: "0001", 369 }) 370 371 // we expect that returned []byte payload can be unmarshalled to *schema.CommercialPaper entity 372 paper := expectcc.PayloadIs(queryResponse, &schema.CommercialPaper{}).(*schema.CommercialPaper) 373 374 Expect(paper.Issuer).To(Equal(IssuerName)) 375 Expect(paper.Owner).To(Equal(IssuerName)) 376 Expect(paper.State).To(Equal(schema.CommercialPaper_ISSUED)) 377 Expect(paper.PaperNumber).To(Equal("0001")) 378 Expect(paper.FaceValue).To(BeNumerically("==", 100000)) 379 }) 380 ``` 381 382 ### Test chaincode permissioning 383 384 Each user can have different permissions to work with chaincode methods. All permissioning is based on user certificates 385 and Membership Service Provider Identifiers, which means that permissions always correspond to an X.509 certificate. 386 387 The simple [car](../examples/cars) contains logic to control who can invoke `carRegister` method. 388 [Test](../examples/cars/cars_test.go) use `From` MockStub method to set certificate and MSP id of invoker 389 390 ```go 391 392 It("Disallow non authority to add information about car", func() { 393 //invoke chaincode method from non authority actor 394 expectcc.ResponseError( 395 cc.From(actors[`someone`]).Invoke(`carRegister`, cars.Payloads[0]), 396 ContainSubstring(owner.ErrOwnerOnly.Error())) // expect "only owner" error 397 }) 398 399 It("Allow authority to add information about car", func() { 400 //invoke chaincode method from authority actor 401 expectcc.ResponseOk(cc.From(actors[`authority`]).Invoke(`carRegister`, cars.Payloads[0])) 402 }) 403 404 ``` 405 406 407 408 ### Debugging operations with chaincode 409 410 During test execution you may need to debug operations in chaincode method handler. `CCKit` supports 411 Hyperledger Fabric 412 [Chaincode Logger](https://github.com/hyperledger/fabric/blob/release-1.4/core/chaincode/shim/chaincode.go#L1105) 413 and its options, so you can use `CORE_CHAINCODE_LOGGING_LEVEL` environment 414 variable. `CCKit` [chaincode state wrapper](../state) outputs debug severity level messages, for example: 415 416 ```shell 417 ## CORE_CHAINCODE_LOGGING_LEVEL=debug go test 418 Running Suite: Commercial Paper Suite 419 ===================================== 420 Random Seed: 1559680577 421 Will run 7 of 7 specs 422 423 2019-06-04 ... [commercial_paper] Debug -> DEBU 001 router handler: init 424 2019-06-04 ... [commercial_paper] Debugf -> DEBU 002 state KEY: [OWNER] 425 2019-06-04 ... [commercial_paper] Debugf -> DEBU 003 state check EXISTENCE OWNER 426 2019-06-04 ... [commercial_paper] Debugf -> DEBU 007 state PUT with string key: OWNER 427 2019-06-04 ... [commercial_paper] Debug -> DEBU 008 router handler: issue 428 2019-06-04 ... [commercial_paper] Debugf -> DEBU 009 state KEY: [_idx CommercialPaper ExternalId EXT0001] 429 2019-06-04 ... [commercial_paper] Debugf -> DEBU 00a state check EXISTENCE _idxCommercialPaperExternalIdEXT0001 430 2019-06-04 ... [commercial_paper] Debugf -> DEBU 00b state KEY: [_idx CommercialPaper ExternalId EXT0001] 431 2019-06-04 ... [commercial_paper] Debugf -> DEBU 00c state PUT with string key: _idxCommercialPaperExternalIdEXT0001 432 ``` 433 434 It shows there are several operations with chaincode state performed while chaincode execution: 435 * checked existence entry with `OWNER` key while handling `init` chaincode method 436 * putted state entry with `OWNER` key 437 * putted state entry with `[_idx CommercialPaper ExternalId EXT0001]` while handling `issue` chaincode method 438 (no uniq index for `Commercial entry` entity) 439 ... 440 441 ## Running test 442 443 To run the test suite you have to simply run the command in the repository where the test suite is located: 444 445 `go test` 446 447 if you have any failures in test you can use `-ginkgo.failFast` option to disable running additional tests 448 after any test fails. 449 450 451 ## Conclusion 452 453 Chaincode `MockStub` is really useful as it allows a developer to test his chaincode without starting the network every time. 454 This reduces development time as he can use a test driven development (TDD) approach where he doesn’t 455 need to start the network (this takes +- 40-80 seconds depending on the specs of the computer).