Adapters, a nicer way to test CorDapps
I’m Ian and I work as a Solution Engineer at R3. One perennial problem for our customers is how to efficiently test CorDapps. Tests are often slow, full of boilerplate style code, and hard to debug. Corda ships with mocks and utilities to take some of the pain away, including Mock Services, Mock Networks, and Deploy Nodes, but they all have their own limitations.
Recently, I was working on a tokenization solution similar to R3’s own Tokens SDK, but with a slightly different audience and target feature set. This project provided an opportunity to explore the Adapter pattern. So what does this mean? It’s a very simple principle made popular by Dependency Injection (DI) frameworks and TDD. First, define an interface that describes the API calls needed in the language of the client, then provide two implementations: one calling the real API and a second version for use in tests. Strictly speaking, this is a test double, but we tend to use the term ‘mock’ generically most of the time. If you are thinking, “great idea, but Corda doesn’t use a DI framework, so how do I make this work?”, then don’t worry; we are fine without having to add Spring, Google Guice, or whatever is the on-trend DI library to our CorDapps.
I’ll use token selection as an example. For those not familiar with tokens, this is the problem of efficiently selecting the set of fungible tokens that add up to at least the amount requested. For example, if the request is for $100 USD, this might find tokens with values of 25, 35, and 45 dollars in the vault. In this example the total equals $105 USD, so one of those tokens would need to be split to give back $5 USD change. At scale, this is a hard problem as there may be very different spending patterns. Perhaps the next request is for $1,000,000 USD, and if so, it would be expensive fulfilling that with lots of small value tokens. A production-quality implementation also needs some form of “soft lock” to stop tokens from being used in multiple parallel transactions at the same time.
The signature of this method in the Tokens SDK is as below:
This single method manages all requests and as you can see has been very generalized, ergo it will need a lot of tests. Multiple spend requests can be passed in (the `recipients`) and the result is the data, inputs, and outputs, needed for the UTXO transaction that will complete the spending request(s) plus any change for the `changeHolder` (the left over 5USD in the earlier example)
Testing this logic as is, is rather difficult because the implementation requires a `ServiceHub` instance as a receiver. The `ServiceHub` provides numerous APIs into services such as the vault, making it cumbersome to mock. The `generateMove` method will be called from within a flow, so we need to use Mock Networks tests and these are fairly expensive in setup/teardown time. Then we need to get the network into the correct state. In the case of tests around fungibility, issuing tokens to the party calling the function under test is a prerequisite. For this customer, we are using the business network membership service to implement role-based access control, so the first step is to set up the membership groups. Then for each scenario, we have to run the issuance flows to set up the tokens, the movement flow which calls `generateMove`, and finally vault queries to assert that the outputs were as expected. And, I almost forgot…! For robust tests, we need a strategy to isolate the data for each scenario so they run independently, for example, scenario one might use USD, scenario two GBP, and so on. Otherwise, test isolation entails tearing down the mock network and bringing up a new version of it, a process that is computationally costly.
All in all, this is a lot of work. The test will be slow to start (we are probably looking at around 15–30 seconds to set up everything and then several seconds per scenario) and the tests are written in the language of flows and vault queries, not `generateMove`. We are one step removed from the code we are testing.
So, the 64,000 dollar question, how do we make this easier to test? Well, it is surprisingly simple. We define an adaptor and inject it in. In this case, the change is trivial, as the adapter only makes a single call to the Corda Service hub.
We have included an adapter, and told Kotlin that the default is the version that will run the necessary query against the vault. It’s not quite production-quality, but you can see the idea.
We also define a `test double`. This needs a little bit of logic to build up the Corda data structures. I suspect this is fairly typical, i.e. we may be able to hide the actual API call, however, some of the parameters will still depend on Corda types. In this case, it is quite easy to mock, but that’s not always true. On my mental to-do list is a little set of builder-style classes I’m calling `Data Angels` that make this as trivial as possible.
Test cases are now just regular JUnit tests and run almost instantly as we no longer require the expensive MockNetwork. An example is below:
Now, these are the kind of tests that make red, green, refactor TDD viable. Each test case is taking just a few milliseconds and is completely isolated. Of course, we will still need some real flow tests to prove the integration with `generateMove`, but that can be limited to just a small number of scenarios. And hopefully, it can be combined with other integration tests to minimize the setup/teardown time.
I hope you found this useful. A code example can be found at https://github.com/mycordaapp/tokens-sdk-demo.
— Ian Morgan is a Solutions Engineer at R3, an enterprise blockchain software firm working with a global ecosystem of more than 350 participants across multiple industries from both the private and public sectors to develop on Corda, its open-source blockchain platform, and Corda Enterprise, a commercial version of Corda for enterprise usage.