diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index a5d0733652..919631dfa0 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -19,17 +19,31 @@ repositories { } } +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + sourceSets { main { resources { srcDir "../../../config/dev" } } + + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } } dependencies { compile project(':core') compile project(':client') + compile project(':test-utils') compile "org.graphstream:gs-core:1.3" compile("org.graphstream:gs-ui:1.3") { @@ -52,3 +66,8 @@ applicationDistribution.into("bin") { from(getClientRpcTutorial) fileMode = 0755 } + +task integrationTest(type: Test) { + testClassesDir = sourceSets.integrationTest.output.classesDir + classpath = sourceSets.integrationTest.runtimeClasspath +} \ No newline at end of file diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt new file mode 100644 index 0000000000..64b2902048 --- /dev/null +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -0,0 +1,113 @@ +package net.corda.docs + +import net.corda.client.CordaRPCClient +import net.corda.contracts.asset.Cash +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.issuedBy +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.Vault +import net.corda.core.serialization.OpaqueBytes +import net.corda.flows.CashCommand +import net.corda.flows.CashFlow +import net.corda.flows.CashFlowResult +import net.corda.node.driver.driver +import net.corda.node.services.User +import net.corda.node.services.config.configureTestSSL +import net.corda.node.services.messaging.ArtemisMessagingComponent +import net.corda.node.services.messaging.startFlow +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.testing.expect +import net.corda.testing.expectEvents +import net.corda.testing.parallel +import net.corda.testing.sequence +import org.junit.Test +import kotlin.concurrent.thread +import kotlin.test.assertEquals + +class IntegrationTestingTutorial { + + @Test + fun aliceBobCashExchangeExample() { + // START 1 + driver { + val testUser = User("testUser", "testPassword", permissions = setOf(startFlowPermission())) + val aliceFuture = startNode("Alice", rpcUsers = listOf(testUser)) + val bobFuture = startNode("Bob", rpcUsers = listOf(testUser)) + val notaryFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + val alice = aliceFuture.get() + val bob = bobFuture.get() + val notary = notaryFuture.get() + // END 1 + + // START 2 + val aliceClient = CordaRPCClient( + host = ArtemisMessagingComponent.toHostAndPort(alice.nodeInfo.address), + config = configureTestSSL() + ) + aliceClient.start("testUser", "testPassword") + val aliceProxy = aliceClient.proxy() + val bobClient = CordaRPCClient( + host = ArtemisMessagingComponent.toHostAndPort(bob.nodeInfo.address), + config = configureTestSSL() + ) + bobClient.start("testUser", "testPassword") + val bobProxy = bobClient.proxy() + // END 2 + + // START 3 + val bobVaultUpdates = bobProxy.vaultAndUpdates().second + val aliceVaultUpdates = aliceProxy.vaultAndUpdates().second + // END 3 + + // START 4 + val issueRef = OpaqueBytes.of(0) + for (i in 1 .. 10) { + thread { + aliceProxy.startFlow(::CashFlow, CashCommand.IssueCash( + amount = i.DOLLARS, + issueRef = issueRef, + recipient = bob.nodeInfo.legalIdentity, + notary = notary.nodeInfo.notaryIdentity + )) + } + } + + bobVaultUpdates.expectEvents { + parallel( + (1 .. 10).map { i -> + expect( + match = { update: Vault.Update -> + (update.produced.first().state.data as Cash.State).amount.quantity == i * 100L + } + ) { update -> + println("Bob vault update of $update") + } + } + ) + } + // END 4 + + // START 5 + for (i in 1 .. 10) { + val flowHandle = bobProxy.startFlow(::CashFlow, CashCommand.PayCash( + amount = i.DOLLARS.issuedBy(alice.nodeInfo.legalIdentity.ref(issueRef)), + recipient = alice.nodeInfo.legalIdentity + )) + assert(flowHandle.returnValue.toBlocking().first() is CashFlowResult.Success) + } + + aliceVaultUpdates.expectEvents { + sequence( + (1 .. 10).map { i -> + expect { update: Vault.Update -> + println("Alice got vault update of $update") + assertEquals((update.produced.first().state.data as Cash.State).amount.quantity, i * 100L) + } + } + ) + } + } + } +} +// END 5 \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 4469b53b4d..2f4d193289 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -68,6 +68,7 @@ Read on to learn: tutorial-contract tutorial-contract-clauses tutorial-test-dsl + tutorial-integration-testing tutorial-clientrpc-api flow-state-machines flow-testing @@ -96,6 +97,7 @@ Read on to learn: :caption: Appendix loadtesting + setting-up-a-corda-network secure-coding-guidelines release-process release-notes diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst new file mode 100644 index 0000000000..4cca59f009 --- /dev/null +++ b/docs/source/tutorial-integration-testing.rst @@ -0,0 +1,117 @@ +Integration Test Tutorial +========================= + +Integration testing involves bringing up nodes locally and testing +invariants about them by starting flows and inspecting their state. + +In this tutorial we 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. We will see how to test some simple deterministic and +nondeterministic invariants in the meantime. + +(Note that this example where Alice is self-issuing Cash is purely for +demonstration purposes, in reality Cash would be issued by a bank and +subsequently passed around.) + +In order to spawn nodes we will use the Driver DSL. This DSL allows +one to start up node processes from code. It manages a network map +service and safe shutting down of nodes in the background. + +.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 1 + :end-before: END 1 + +The above code creates a ``User`` permissioned to start the +``CashFlow`` protocol. It then starts up Alice and Bob with this user, +allowing us to later connect to the nodes. + +Then the notary is started up. Note that we need to add +``ValidatingNotaryService`` as an advertised service in order for this +node to serve notary functionality. This is also where flows added in +plugins should be specified. Note also that we won't connect to the +notary directly, so there's no need to pass in the test ``User``. + +The ``startNode`` function returns a future that completes once the +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 ``NodeInfo`` s. + +.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 2 + :end-before: END 2 + +Next we connect to Alice and Bob respectively from the test process +using the test user we created. Then we establish RPC links that allow +us to start flows and query state. + +Note that Driver nodes start up with test server certificiates, so +it's enough to pass in ``configureTestSSL()`` for the clients. + +.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 3 + :end-before: END 3 + +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! + +.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 4 + :end-before: END 4 + +The first loop creates 10 threads, each starting a ``CashFlow`` flow +on the Alice node. We specify that we want to issue ``i`` dollars to +Bob, using the Notary as the notary responsible for notarising the +created states. Note that no notarisation will occur yet as we're not +spending any states, only entering new ones to the ledger. + +We started the flows from different threads for the sake of the +tutorial, to demonstrate how to test non-determinism, which is what +the ``expectEvents`` block does. + +The Expect DSL allows ordering constraints to be checked on a stream +of events. The above code specifies that we are expecting 10 updates +to be emitted on the ``bobVaultUpdates`` stream in unspecified order +(this is what the ``parallel`` construct does). We specify a +(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 +(Alice,Bob,Notary + implicit Network Map service), then 10 logs of Bob +receiving 1,2,...10 dollars from Alice in some unspecified order. + +Next we want Bob to send this Cash back to Alice. + +.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 5 + :end-before: END 5 + +This time we'll do it sequentially. We make Bob pay 1,2,..10 dollars +to Alice in order. We make sure that a the ``CashFlow`` has finished +by waiting on ``startFlow`` 's ``returnValue``. + +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 diff --git a/test-utils/src/main/kotlin/net/corda/testing/Expect.kt b/test-utils/src/main/kotlin/net/corda/testing/Expect.kt index 4da88de05d..60371000ae 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/Expect.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/Expect.kt @@ -68,6 +68,7 @@ inline fun expect( * @param expectations The pieces of DSL that should run sequentially when events arrive. */ fun sequence(vararg expectations: ExpectCompose): ExpectCompose = ExpectCompose.Sequential(listOf(*expectations)) +fun sequence(expectations: List>): ExpectCompose = ExpectCompose.Sequential(expectations) /** * Tests that events arrive in unspecified order. @@ -75,6 +76,7 @@ fun sequence(vararg expectations: ExpectCompose): ExpectCompose = Expe * @param expectations The pieces of DSL all of which should run but in an unspecified order depending on what sequence events arrive. */ fun parallel(vararg expectations: ExpectCompose): ExpectCompose = ExpectCompose.Parallel(listOf(*expectations)) +fun parallel(expectations: List>): ExpectCompose = ExpectCompose.Parallel(expectations) /** * Tests that N events of the same type arrive