Integration testing
This tutorial will take you through the steps involved in conducting integration testing on your CorDapp.
Introduction
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
Integration testing involves bringing up nodes locally and testing invariants about them by starting flows and inspecting their state.
In this tutorial, you will bring up three nodes - Alice, Bob, and a notary. Alice will issue cash to Bob, then Bob will send this cash back to Alice. You will see how to test some simple deterministic and nondeterministic invariants in the meantime.
Starting the nodes
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
In order to spawn nodes, you will use the Driver DSL. This DSL allows you to start up node processes from code. It creates a local network where all the nodes see each other and enables the safe shutting down of nodes in the background.
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = FINANCE_CORDAPPS)) {
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
startFlow<CashIssueAndPaymentFlow>(),
invokeRpc("vaultTrackBy")
))
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
startFlow<CashPaymentFlow>(),
invokeRpc("vaultTrackBy")
))
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)),
startNode(providedName = BOB_NAME, rpcUsers = listOf(bobUser))
).map { it.getOrThrow() }
driver(new DriverParameters()
.withStartNodesInProcess(true)
.withCordappsForAllNodes(FINANCE_CORDAPPS), dsl -> {
User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList(
startFlow(CashIssueAndPaymentFlow.class),
invokeRpc("vaultTrack")
)));
User bobUser = new User("bobUser", "testPassword2", new HashSet<>(asList(
startFlow(CashPaymentFlow.class),
invokeRpc("vaultTrack")
)));
try {
List<CordaFuture<NodeHandle>> nodeHandleFutures = asList(
dsl.startNode(new NodeParameters().withProvidedName(ALICE_NAME).withRpcUsers(singletonList(aliceUser))),
dsl.startNode(new NodeParameters().withProvidedName(BOB_NAME).withRpcUsers(singletonList(bobUser)))
);
NodeHandle alice = nodeHandleFutures.get(0).get();
NodeHandle bob = nodeHandleFutures.get(1).get();
The above code starts two nodes:
- A node for Alice, configured with an RPC user who has permissions to start the
CashIssueAndPaymentFlow
flow on it and query Alice’s vault. - A node for Bob, configured with an RPC user who only has permissions to start the
CashPaymentFlow
and query Bob’s vault.
DUMMY_NOTARY_NAME
which is visible to both nodes. If you wish to customise this, for
example, to create more notaries, then specify the DriverParameters.notarySpecs
parameter.The startNode
function returns a CordaFuture
object that completes once the node is fully started and visible on
the local network. Returning a future allows starting of the nodes to be parallel. You must wait for the CordaFuture
objects to complete, as to proceed, you will need the NodeHandles
for each object.
val aliceClient = CordaRPCClient(alice.rpcAddress)
val aliceProxy: CordaRPCOps = aliceClient.start("aliceUser", "testPassword1").proxy
val bobClient = CordaRPCClient(bob.rpcAddress)
val bobProxy: CordaRPCOps = bobClient.start("bobUser", "testPassword2").proxy
CordaRPCClient aliceClient = new CordaRPCClient(alice.getRpcAddress());
CordaRPCOps aliceProxy = aliceClient.start("aliceUser", "testPassword1").getProxy();
CordaRPCClient bobClient = new CordaRPCClient(bob.getRpcAddress());
CordaRPCOps bobProxy = bobClient.start("bobUser", "testPassword2").getProxy();
Connecting to each node via RPC
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
Next, you must connect to Alice and Bob from the test process using the test users created earlier. To be able to start flows and query states, you must establish an RPC connection to each node.
val bobVaultUpdates: Observable<Vault.Update<Cash.State>> = bobProxy.vaultTrackBy<Cash.State>().updates
val aliceVaultUpdates: Observable<Vault.Update<Cash.State>> = aliceProxy.vaultTrackBy<Cash.State>().updates
Observable<Vault.Update<Cash.State>> bobVaultUpdates = bobProxy.vaultTrack(Cash.State.class).getUpdates();
Observable<Vault.Update<Cash.State>> aliceVaultUpdates = aliceProxy.vaultTrack(Cash.State.class).getUpdates();
Monitoring changes to the vaults
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
You will be interested in changes to Alice’s and Bob’s vault, so you need to set up queries to return a stream of vault updates from each.
val issueRef = OpaqueBytes.of(0)
aliceProxy.startFlow(::CashIssueAndPaymentFlow,
1000.DOLLARS,
issueRef,
bob.nodeInfo.singleIdentity(),
true,
defaultNotaryIdentity
).returnValue.getOrThrow()
bobVaultUpdates.expectEvents {
expect { update ->
println("Bob got vault update of $update")
val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount
assertEquals(1000.DOLLARS, amount.withoutIssuer())
}
}
OpaqueBytes issueRef = OpaqueBytes.of((byte)0);
aliceProxy.startFlowDynamic(
CashIssueAndPaymentFlow.class,
DOLLARS(1000),
issueRef,
bob.getNodeInfo().getLegalIdentities().get(0),
true,
dsl.getDefaultNotaryIdentity()
).getReturnValue().get();
@SuppressWarnings("unchecked")
Class<Vault.Update<Cash.State>> cashVaultUpdateClass = (Class<Vault.Update<Cash.State>>)(Class<?>)Vault.Update.class;
expectEvents(bobVaultUpdates, true, () ->
expect(cashVaultUpdateClass, update -> true, update -> {
System.out.println("Bob got vault update of " + update);
Amount<Issued<Currency>> amount = update.getProduced().iterator().next().getState().getData().getAmount();
assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount));
return null;
})
);
Starting a flow
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
Now that you’re all set up, you can finally get some cash action going!
The code in the example below will start a CashIssueAndPaymentFlow
flow on the Alice node. It specifies that you want Alice to self-issue $1000 which is
to be paid to Bob. It also specifies that the default notary identity created by the driver is the notary responsible for notarising
the created states. Note that no notarisation will occur yet, as you’re not spending any states - you’re only creating new ones on
the ledger.
We expect a single update to Bob’s vault when it receives the $1000 from Alice. This is what the expectEvents
call
is asserting.
bobProxy.startFlow(::CashPaymentFlow, 1000.DOLLARS, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
aliceVaultUpdates.expectEvents {
expect { update ->
println("Alice got vault update of $update")
val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount
assertEquals(1000.DOLLARS, amount.withoutIssuer())
}
}
bobProxy.startFlowDynamic(
CashPaymentFlow.class,
DOLLARS(1000),
alice.getNodeInfo().getLegalIdentities().get(0)
).getReturnValue().get();
expectEvents(aliceVaultUpdates, true, () ->
expect(cashVaultUpdateClass, update -> true, update -> {
System.out.println("Alice got vault update of " + update);
Amount<Issued<Currency>> amount = update.getProduced().iterator().next().getState().getData().getAmount();
assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount));
return null;
})
);
As a next step, you might like to try setting up a test where Bob sends this cash back to Alice.
Testing Corda 4.11 to 4.12 migration
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
To create a transaction that is backward compatible with Corda 4.11 nodes requires attaching both the 4.11 and 4.12 contract JARs. Since a state can only have one attachment constraint, you cannot use hash constraints because they can only match one of the contracts. Therefore, backward-compatible transactions must use signature constraints, where both contract JARs are signed by a common signer. This ensures that the same signature constraint satisfies both the legacy and current transaction components.
The problem comes during development and testing. The signing of the CorDapp may be not be possible during development but just before a release: for example, because the signing process uses a HSM. Therefore, you must be able to test that your Corda 4.11 to 4.12 migration works. You can do it using the node driver.
To do this, sign TestCordapps
with the introduction of a TestCordapp.asSigned()
method that creates a copy of the JAR and signs it with the dev key.
The node driver also adds support for the new legacy-contracts
directory. This is done with a new NodeParameters
property, val legacyContracts: Collection<TestCordapp> = emptySet()
.
TestCordapp
discovery had also been improved: the current method TestCordapp.findCordapp
takes a package name and scans the current classpath to find the single JAR containing that given package. This does not work here as both the 4.11 and 4.12 CorDapps have the same package namespace. This has been solved via a new way to reference CorDapps for the node driver: TestCodapp.of(URI)
.
Summary
![Click to copy the section link. heading-link-icon](/icons/chain-icon.png)
That’s it! You saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants
about CashIssueAndPaymentFlow
and CashPaymentFlow
.
You can find the complete test at example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
(Java) and example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
(Kotlin) in the
Corda repo.
To make sure the driver classes are included in your project, you will need to include the following in the build.gradle
file in the module in
which you want to test:
testCompile "$corda_release_group:corda-node-driver:$corda_release_version"
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.