docs: Add integration test tutorial

This commit is contained in:
Andras Slemmer 2016-11-25 15:26:00 +00:00
parent 56ada37259
commit 220ccdbe13
5 changed files with 207 additions and 0 deletions

View File

@ -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
}

View File

@ -0,0 +1,112 @@
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
class IntegrationTestingTutorial {
@Test
fun aliceBobCashExchangeExample() {
// START 1
driver {
val testUser = User("testUser", "testPassword", permissions = setOf(startFlowPermission<CashFlow>()))
val aliceFuture = startNode("Alice", rpcUsers = listOf(testUser))
val bobFuture = startNode("Bob", rpcUsers = listOf(testUser))
val notaryFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.Companion.type)))
val alice = aliceFuture.get()
val bob = bobFuture.get()
val notary = notaryFuture.get()
// END 1
// START 2
val aliceClient = CordaRPCClient(
host = ArtemisMessagingComponent.Companion.toHostAndPort(alice.nodeInfo.address),
config = configureTestSSL()
)
aliceClient.start("testUser", "testPassword")
val aliceProxy = aliceClient.proxy()
val bobClient = CordaRPCClient(
host = ArtemisMessagingComponent.Companion.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.Companion.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
))
require(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")
require((update.produced.first().state.data as Cash.State).amount.quantity == i * 100L)
}
}
)
}
}
}
}
// END 5

View File

@ -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

View File

@ -0,0 +1,73 @@
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.
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

View File

@ -67,6 +67,7 @@ inline fun <reified E : Any> expect(
* @param expectations The pieces of DSL that should run sequentially when events arrive.
*/
fun <E> sequence(vararg expectations: ExpectCompose<E>): ExpectCompose<E> = ExpectCompose.Sequential(listOf(*expectations))
fun <E> sequence(expectations: List<ExpectCompose<E>>): ExpectCompose<E> = ExpectCompose.Sequential(expectations)
/**
* Tests that events arrive in unspecified order.
@ -74,6 +75,7 @@ fun <E> sequence(vararg expectations: ExpectCompose<E>): ExpectCompose<E> = Expe
* @param expectations The pieces of DSL all of which should run but in an unspecified order depending on what sequence events arrive.
*/
fun <E> parallel(vararg expectations: ExpectCompose<E>): ExpectCompose<E> = ExpectCompose.Parallel(listOf(*expectations))
fun <E> parallel(expectations: List<ExpectCompose<E>>): ExpectCompose<E> = ExpectCompose.Parallel(expectations)
/**
* Tests that N events of the same type arrive