Fixed out-of-date integration testing tutorial and added Java example (#3596)

This commit is contained in:
Shams Asari
2018-07-16 13:14:57 +01:00
committed by GitHub
parent d672cba877
commit ca56f6bd33
4 changed files with 324 additions and 208 deletions

View File

@ -0,0 +1,124 @@
package net.corda.docs;
import net.corda.client.rpc.CordaRPCClient;
import net.corda.core.concurrent.CordaFuture;
import net.corda.core.contracts.Amount;
import net.corda.core.contracts.Issued;
import net.corda.core.contracts.Structures;
import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.node.services.Vault;
import net.corda.core.utilities.OpaqueBytes;
import net.corda.finance.contracts.asset.Cash;
import net.corda.finance.flows.CashIssueAndPaymentFlow;
import net.corda.finance.flows.CashPaymentFlow;
import net.corda.testing.driver.DriverParameters;
import net.corda.testing.driver.NodeHandle;
import net.corda.testing.driver.NodeParameters;
import net.corda.testing.node.User;
import org.junit.Test;
import rx.Observable;
import java.util.Currency;
import java.util.HashSet;
import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.node.services.Permissions.invokeRpc;
import static net.corda.node.services.Permissions.startFlow;
import static net.corda.testing.core.ExpectKt.expect;
import static net.corda.testing.core.ExpectKt.expectEvents;
import static net.corda.testing.core.TestConstants.ALICE_NAME;
import static net.corda.testing.core.TestConstants.BOB_NAME;
import static net.corda.testing.driver.Driver.driver;
import static org.junit.Assert.assertEquals;
public class JavaIntegrationTestingTutorial {
@Test
public void aliceBobCashExchangeExample() {
// START 1
driver(new DriverParameters()
.withStartNodesInProcess(true)
.withExtraCordappPackagesToScan(singletonList("net.corda.finance.contracts.asset")), 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();
// END 1
// START 2
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();
// END 2
// START 3
Observable<Vault.Update<Cash.State>> bobVaultUpdates = bobProxy.vaultTrack(Cash.State.class).getUpdates();
Observable<Vault.Update<Cash.State>> aliceVaultUpdates = aliceProxy.vaultTrack(Cash.State.class).getUpdates();
// END 3
// START 4
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;
})
);
// END 4
// START 5
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;
})
);
// END 5
} catch (Exception e) {
throw new RuntimeException("Exception thrown in driver DSL", e);
}
return null;
});
}
}

View File

@ -1,114 +0,0 @@
package net.corda.docs
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultTrackBy
import net.corda.core.node.services.Vault
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.*
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import org.junit.Test
import kotlin.test.assertEquals
class IntegrationTestingTutorial {
@Test
fun `alice bob cash exchange example`() {
// START 1
driver(DriverParameters(startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset"))) {
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
startFlow<CashIssueFlow>(),
startFlow<CashPaymentFlow>(),
invokeRpc("vaultTrackBy"),
invokeRpc(CordaRPCOps::notaryIdentities),
invokeRpc(CordaRPCOps::networkMapFeed)
))
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
startFlow<CashPaymentFlow>(),
invokeRpc("vaultTrackBy"),
invokeRpc(CordaRPCOps::networkMapFeed)
))
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)),
startNode(providedName = BOB_NAME, rpcUsers = listOf(bobUser))
).transpose().getOrThrow()
// END 1
// START 2
val aliceClient = CordaRPCClient(alice.rpcAddress)
val aliceProxy = aliceClient.start("aliceUser", "testPassword1").proxy
val bobClient = CordaRPCClient(bob.rpcAddress)
val bobProxy = bobClient.start("bobUser", "testPassword2").proxy
// END 2
// START 3
val bobVaultUpdates = bobProxy.vaultTrackBy<Cash.State>().updates
val aliceVaultUpdates = aliceProxy.vaultTrackBy<Cash.State>().updates
// END 3
// START 4
val issueRef = OpaqueBytes.of(0)
val notaryParty = aliceProxy.notaryIdentities().first()
(1..10).map { i ->
aliceProxy.startFlow(::CashIssueFlow,
i.DOLLARS,
issueRef,
notaryParty
).returnValue
}.transpose().getOrThrow()
// We wait for all of the issuances to run before we start making payments
(1..10).map { i ->
aliceProxy.startFlow(::CashPaymentFlow,
i.DOLLARS,
bob.nodeInfo.singleIdentity(),
true
).returnValue
}.transpose().getOrThrow()
bobVaultUpdates.expectEvents {
parallel(
(1..10).map { i ->
expect(
match = { update: Vault.Update<Cash.State> ->
update.produced.first().state.data.amount.quantity == i * 100L
}
) { update ->
println("Bob vault update of $update")
}
}
)
}
// END 4
// START 5
for (i in 1..10) {
bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
aliceVaultUpdates.expectEvents {
sequence(
(1..10).map { i ->
expect { update: Vault.Update<Cash.State> ->
println("Alice got vault update of $update")
assertEquals(update.produced.first().state.data.amount.quantity, i * 100L)
}
}
)
}
// END 5
}
}
}

View File

@ -0,0 +1,97 @@
package net.corda.docs
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued
import net.corda.core.contracts.withoutIssuer
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultTrackBy
import net.corda.core.node.services.Vault
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.*
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import org.junit.Test
import rx.Observable
import java.util.*
import kotlin.test.assertEquals
class KotlinIntegrationTestingTutorial {
@Test
fun `alice bob cash exchange example`() {
// START 1
driver(DriverParameters(
startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")
)) {
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() }
// END 1
// START 2
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
// END 2
// START 3
val bobVaultUpdates: Observable<Vault.Update<Cash.State>> = bobProxy.vaultTrackBy<Cash.State>().updates
val aliceVaultUpdates: Observable<Vault.Update<Cash.State>> = aliceProxy.vaultTrackBy<Cash.State>().updates
// END 3
// START 4
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())
}
}
// END 4
// START 5
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())
}
}
// END 5
}
}
}

View File

@ -1,119 +1,128 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Integration testing Integration testing
=================== ===================
Integration testing involves bringing up nodes locally and testing Integration testing involves bringing up nodes locally and testing invariants about them by starting flows and inspecting
invariants about them by starting flows and inspecting their state. their state.
In this tutorial we will bring up three nodes - Alice, Bob and a In this tutorial we will bring up three nodes - Alice, Bob and a notary. Alice will issue cash to Bob, then Bob will send
notary. Alice will issue cash to Bob, then Bob will send this cash this cash back to Alice. We will see how to test some simple deterministic and nondeterministic invariants in the meantime.
back to Alice. We will see how to test some simple deterministic and
nondeterministic invariants in the meantime.
.. note:: This example where Alice is self-issuing cash is purely for .. note:: This example where Alice is self-issuing cash is purely for demonstration purposes, in reality, cash would be
demonstration purposes, in reality, cash would be issued by a bank issued by a bank and subsequently passed around.
and subsequently passed around.
In order to spawn nodes we will use the Driver DSL. This DSL allows In order to spawn nodes we will use the Driver DSL. This DSL allows one to start up node processes from code. It creates
one to start up node processes from code. It manages a network map a local network where all the nodes see each other and provides safe shutting down of nodes in the background.
service and safe shutting down of nodes in the background.
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt .. container:: codeset
:language: kotlin
:start-after: START 1
:end-before: END 1
:dedent: 8
The above code starts three nodes: .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
:language: kotlin
:start-after: START 1
:end-before: END 1
:dedent: 8
* Alice, who has user permissions to start the ``CashIssueFlow`` and .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
``CashPaymentFlow`` flows :language: java
* Bob, who only has user permissions to start the ``CashPaymentFlow`` :start-after: START 1
* A notary that offers a ``ValidatingNotaryService``. We won't connect :end-before: END 1
to the notary directly, so there's no need to provide a ``User`` :dedent: 8
The ``startNode`` function returns a future that completes once the The above code starts two nodes:
node is fully started. This allows starting of the nodes to be
parallel. We wait on these futures as we need the information
returned; their respective ``NodeHandles`` s.
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt * Alice, configured with an RPC user who has permissions to start the ``CashIssueAndPaymentFlow`` flow on it and query
:language: kotlin Alice's vault.
:start-after: START 2 * Bob, configured with an RPC user who only has permissions to start the ``CashPaymentFlow`` and query Bob's vault.
:end-before: END 2
:dedent: 12
After getting the handles we wait for both parties to register with .. note:: You will notice that we did not start a notary. This is done automatically for us by the driver - it creates
the network map to ensure we don't have race conditions with network a notary node with the name ``DUMMY_NOTARY_NAME`` which is visible to both nodes. If you wish to customise this, for
map registration. Next we connect to Alice and Bob respectively from example create more notaries, then specify the ``DriverParameters.notarySpecs`` parameter.
the test process using the test user we created. Then we establish RPC
links that allow us to start flows and query state.
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt The ``startNode`` function returns a ``CordaFuture`` object that completes once the node is fully started and visible on
:language: kotlin the local network. Returning a future allows starting of the nodes to be parallel. We wait on these futures as we need
:start-after: START 3 the information returned; their respective ``NodeHandles`` s.
:end-before: END 3
:dedent: 12
We will be interested in changes to Alice's and Bob's vault, so we .. container:: codeset
query a stream of vault updates from each.
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
:language: kotlin
:start-after: START 2
:end-before: END 2
:dedent: 12
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
:language: java
:start-after: START 2
:end-before: END 2
:dedent: 16
Next we connect to Alice and Bob from the test process using the test users we created. We establish RPC links that allow
us to start flows and query state.
.. container:: codeset
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
:language: kotlin
:start-after: START 3
:end-before: END 3
:dedent: 12
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
:language: java
:start-after: START 3
:end-before: END 3
:dedent: 16
We will be interested in changes to Alice's and Bob's vault, so we query a stream of vault updates from each.
Now that we're all set up we can finally get some cash action going! Now that we're all set up we can finally get some cash action going!
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt .. container:: codeset
:language: kotlin
:start-after: START 4
:end-before: END 4
:dedent: 12
The first loop creates 10 threads, each starting a ``CashFlow`` flow .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
on the Alice node. We specify that we want to issue ``i`` dollars to :language: kotlin
Bob, setting our notary as the notary responsible for notarising the :start-after: START 4
created states. Note that no notarisation will occur yet as we're not :end-before: END 4
spending any states, only creating new ones on the ledger. :dedent: 12
We started the flows from different threads for the sake of the .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
tutorial, to demonstrate how to test non-determinism, which is what :language: java
the ``expectEvents`` block does. :start-after: START 4
:end-before: END 4
:dedent: 16
The Expect DSL allows ordering constraints to be checked on a stream We start a ``CashIssueAndPaymentFlow`` flow on the Alice node. We specify that we want Alice to self-issue $1000 which is
of events. The above code specifies that we are expecting 10 updates to be payed to Bob. We specify the default notary identity created by the driver as the notary responsible for notarising
to be emitted on the ``bobVaultUpdates`` stream in unspecified order the created states. Note that no notarisation will occur yet as we're not spending any states, only creating new ones on
(this is what the ``parallel`` construct does). We specify an the ledger.
(otherwise optional) ``match`` predicate to identify specific updates
we are interested in, which we then print.
If we run the code written so far we should see 4 nodes starting up We expect a single update to Bob's vault when it receives the $1000 from Alice. This is what the ``expectEvents`` call
(Alice, Bob, the notary and an implicit Network Map service), then is asserting.
10 logs of Bob receiving 1,2,...10 dollars from Alice in some unspecified
order. .. container:: codeset
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
:language: kotlin
:start-after: START 5
:end-before: END 5
:dedent: 12
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
:language: java
:start-after: START 5
:end-before: END 5
:dedent: 16
Next we want Bob to send this cash back to Alice. Next we want Bob to send this cash back to Alice.
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt That's it! We saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants
:language: kotlin about ``CashIssueAndPaymentFlow`` and ``CashPaymentFlow``.
:start-after: START 5
:end-before: END 5
:dedent: 12
This time we'll do it sequentially. We make Bob pay 1,2,..10 dollars You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java``
to Alice in order. We make sure that a the ``CashFlow`` has finished (Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the
by waiting on ``startFlow`` 's ``returnValue``. `Corda repo <https://github.com/corda/corda>`_.
Then we use the Expect DSL again, this time using ``sequence`` to test
for the updates arriving in the order we expect them to.
Note that ``parallel`` and ``sequence`` may be nested into each other
arbitrarily to test more complex scenarios.
That's it! We saw how to start up several corda nodes locally, how to
connect to them, and how to test some simple invariants about
``CashFlow``.
To run the complete test you can open
``example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt``
from IntelliJ and run the test, or alternatively use gradle:
.. sourcecode:: bash
# Run example-code integration tests
./gradlew docs/source/example-code:integrationTest -i