* Add CordaService testing documentation and improve tests in irs-demo * Addressed review comments
21 KiB
API: Testing
Flow testing
MockNetwork
Flow testing can be fully automated using a MockNetwork
composed of StartedMockNode
nodes. Each
StartedMockNode
behaves like a regular Corda node, but its
services are either in-memory or mocked out.
A MockNetwork
is created as follows:
class FlowTests {
private lateinit var mockNet: MockNetwork
@Before
fun setup() {
= MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
network }
}
public class IOUFlowTests {
private MockNetwork network;
@Before
public void setup() {
= new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"));
network }
}
The MockNetwork
requires at a minimum a list of
packages. Each package is packaged into a CorDapp JAR and installed as a
CorDapp on each StartedMockNode
.
Configuring the
MockNetwork
The MockNetwork
is configured automatically. You can
tweak its configuration using a MockNetworkParameters
object, or by using named paramters in Kotlin:
val network = MockNetwork(
// A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
= listOf("my.cordapp.package", "my.other.cordapp.package"),
cordappPackages // If true then each node will be run in its own thread. This can result in race conditions in your
// code if not carefully written, but is more realistic and may help if you have flows in your app that
// do long blocking operations.
= false,
threadPerNode // The notaries to use on the mock network. By default you get one mock notary and that is usually
// sufficient.
= listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)),
notarySpecs // If true then messages will not be routed from sender to receiver until you use the
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can
// examine the state of the mock network before and after a message is sent, without races and without
// the receiving node immediately sending a response.
= false,
networkSendManuallyPumped // How traffic is allocated in the case where multiple nodes share a single identity, which happens for
// notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing
// notary implementations.
= InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
servicePeerAllocationStrategy
val network2 = MockNetwork(
// A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
listOf// If true then each node will be run in its own thread. This can result in race conditions in your
// code if not carefully written, but is more realistic and may help if you have flows in your app that
// do long blocking operations.
= false,
threadPerNode // The notaries to use on the mock network. By default you get one mock notary and that is usually
// sufficient.
= listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)),
notarySpecs // If true then messages will not be routed from sender to receiver until you use the
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can
// examine the state of the mock network before and after a message is sent, without races and without
// the receiving node immediately sending a response.
= false,
networkSendManuallyPumped // How traffic is allocated in the case where multiple nodes share a single identity, which happens for
// notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing
// notary implementations.
= InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
servicePeerAllocationStrategy )
= MockNetwork(
MockNetwork network // A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
.of("my.cordapp.package", "my.other.cordapp.package"),
ImmutableListnew MockNetworkParameters()
// If true then each node will be run in its own thread. This can result in race conditions in
// your code if not carefully written, but is more realistic and may help if you have flows in
// your app that do long blocking operations.
.setThreadPerNode(false)
// The notaries to use on the mock network. By default you get one mock notary and that is
// usually sufficient.
.setNotarySpecs(ImmutableList.of(new MockNetworkNotarySpec(DUMMY_NOTARY_NAME)))
// If true then messages will not be routed from sender to receiver until you use the
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code
// that can examine the state of the mock network before and after a message is sent, without
// races and without the receiving node immediately sending a response.
.setNetworkSendManuallyPumped(false)
// How traffic is allocated in the case where multiple nodes share a single identity, which
// happens for notaries in a cluster. You don't normally ever need to change this: it is mostly
// useful for testing notary implementations.
.setServicePeerAllocationStrategy(new InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()));
Adding nodes to the network
Nodes are created on the MockNetwork
using:
class FlowTests {
private lateinit var mockNet: MockNetwork
lateinit var nodeA: StartedMockNode
lateinit var nodeB: StartedMockNode
@Before
fun setup() {
= MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
network = network.createPartyNode()
nodeA // We can optionally give the node a name.
= network.createPartyNode(CordaX500Name("Bank B", "London", "GB"))
nodeB }
}
public class IOUFlowTests {
private MockNetwork network;
private StartedMockNode a;
private StartedMockNode b;
@Before
public void setup() {
= new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"));
network = network.createPartyNode(null);
nodeA // We can optionally give the node a name.
= network.createPartyNode(new CordaX500Name("Bank B", "London", "GB"));
nodeB }
}
Registering a node's initiated flows
Regular Corda nodes automatically register any response flows defined
in their installed CorDapps. When using a MockNetwork
, each
StartedMockNode
must manually register any responder flows
it wishes to use.
Responder flows are registered as follows:
.registerInitiatedFlow(ExampleFlow.Acceptor::class.java) nodeA
.registerInitiatedFlow(ExampleFlow.Acceptor.class); nodeA
Running the network
Regular Corda nodes automatically process received messages. When
using a MockNetwork
with
networkSendManuallyPumped
set to false
, you
must manually initiate the processing of received messages.
You manually process received messages as follows:
StartedMockNode.pumpReceive
to process a single message from the node's queueMockNetwork.runNetwork
to process all the messages in every node's queue. This may generate additional messages that must in turn be processednetwork.runNetwork(-1)
(the default in Kotlin) will exchange messages until there are no further messages to process
Running flows
A StartedMockNode
starts a flow using the
StartedNodeServices.startFlow
method. This method returns a
future representing the output of running the flow.
val signedTransactionFuture = nodeA.services.startFlow(IOUFlow(iouValue = 99, otherParty = nodeBParty))
<SignedTransaction> future = startFlow(a.getServices(), new ExampleFlow.Initiator(1, nodeBParty)); CordaFuture
The network must then be manually run before retrieving the future's value:
val signedTransactionFuture = nodeA.services.startFlow(IOUFlow(iouValue = 99, otherParty = nodeBParty))
// Assuming network.networkSendManuallyPumped == false.
.runNetwork()
networkval signedTransaction = future.get();
<SignedTransaction> future = startFlow(a.getServices(), new ExampleFlow.Initiator(1, nodeBParty));
CordaFuture// Assuming network.networkSendManuallyPumped == false.
.runNetwork();
network= future.get(); SignedTransaction signedTransaction
Accessing
StartedMockNode
internals
Creating a node database transaction
Whenever you query a node's database (e.g. to extract information from the node's vault), you must wrap the query in a database transaction, as follows:
.database.transaction {
nodeA// Perform query here.
}
.getDatabase().transaction(tx -> {
node// Perform query here.
}
Querying a node's vault
Recorded states can be retrieved from the vault of a
StartedMockNode
using:
.database.transaction {
nodeAval myStates = nodeA.services.vaultService.queryBy<MyStateType>().states
}
.getDatabase().transaction(tx -> {
nodeList<MyStateType> myStates = node.getServices().getVaultService().queryBy(MyStateType.class).getStates();
}
This allows you to check whether a given state has (or has not) been stored, and whether it has the correct attributes.
Examining a node's transaction storage
Recorded transactions can be retrieved from the transaction storage
of a StartedMockNode
using:
val transaction = nodeA.services.validatedTransactions.getTransaction(transaction.id)
= nodeA.getServices().getValidatedTransactions().getTransaction(transaction.getId()) SignedTransaction transaction
This allows you to check whether a given transaction has (or has not) been stored, and whether it has the correct attributes.
This allows you to check whether a given state has (or has not) been stored, and whether it has the correct attributes.
Further examples
- See the flow testing tutorial
here <flow-testing>
- See the oracle tutorial
here <oracles>
for information on testing@CordaService
classes - Further examples are available in the Example CorDapp in Java and Kotlin
Contract testing
The Corda test framework includes the ability to create a test ledger
by calling the ledger
function on an implementation of the
ServiceHub
interface.
Test identities
You can create dummy identities to use in test transactions using the
TestIdentity
class:
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
TestIdentity
exposes the following fields and
methods:
val identityParty: Party = bigCorp.party
val identityName: CordaX500Name = bigCorp.name
val identityPubKey: PublicKey = bigCorp.publicKey
val identityKeyPair: KeyPair = bigCorp.keyPair
val identityPartyAndCertificate: PartyAndCertificate = bigCorp.identity
= bigCorp.getParty();
Party identityParty = bigCorp.getName();
CordaX500Name identityName PublicKey identityPubKey = bigCorp.getPublicKey();
KeyPair identityKeyPair = bigCorp.getKeyPair();
= bigCorp.getIdentity(); PartyAndCertificate identityPartyAndCertificate
You can also create a unique TestIdentity
using the
fresh
method:
val uniqueTestIdentity: TestIdentity = TestIdentity.fresh("orgName")
= TestIdentity.Companion.fresh("orgName"); TestIdentity uniqueTestIdentity
MockServices
A mock implementation of ServiceHub
is provided in
MockServices
. This is a minimal ServiceHub
that suffices to test contract logic. It has the ability to insert
states into the vault, query the vault, and construct and check
transactions.
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
Alternatively, there is a helper constructor which just accepts a
list of TestIdentity
. The first identity provided is the
identity of the node whose ServiceHub
is being mocked, and
any subsequent identities are identities that the node knows about. Only
the calling package is scanned for cordapps and a test
IdentityService
is created for you, using all the given
identities.
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
Writing tests using a test ledger
The ServiceHub.ledger
extension function allows you to
create a test ledger. Within the ledger wrapper you can create
transactions using the transaction
function. Within a
transaction you can define the input
and
output
states for the transaction, alongside any commands
that are being executed, the timeWindow
in which the
transaction has been executed, and any attachments
, as
shown in this example test:
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
Once all the transaction components have been specified, you can run
verifies()
to check that the given transaction is
valid.
Checking for failure states
In order to test for failures, you can use the failsWith
method, or in Kotlin the fails with
helper method, which
assert that the transaction fails with a specific error. If you just
want to assert that the transaction has failed without verifying the
message, there is also a fails
method.
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
Note
The transaction DSL forces the last line of the test to be either a
verifies
or fails with
statement.
Testing multiple scenarios at once
Within a single transaction block, you can assert several times that the transaction constructed so far either passes or fails verification. For example, you could test that a contract fails to verify because it has no output states, and then add the relevant output state and check that the contract verifies successfully, as in the following example:
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
You can also use the tweak
function to create a locally
scoped transaction that you can make changes to and then return to the
original, unmodified transaction. As in the following example:
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java
Chaining transactions
The following example shows that within a ledger
, you
can create more than one transaction
in order to test
chains of transactions. In addition to transaction
,
unverifiedTransaction
can be used, as in the example below,
to create transactions on the ledger without verifying them, for
pre-populating the ledger with existing data. When chaining
transactions, it is important to note that even though a
transaction
verifies
successfully, the overall
ledger may not be valid. This can be verified separately by placing a
verifies
or fails
statement within the
ledger
block.
../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt
../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java