Writing flow tests

This tutorial will take you through the steps required to write a flow test.

Introduction

A flow can be a fairly complex thing that interacts with many services and other parties over the network. This means that unit testing a flow requires some infrastructure to provide lightweight mock implementations.

Creating a mock network

The MockNetwork class provides this testing infrastructure layer; you can find this class in the test-utils module.

The IOUTransferFlowTests tests provide a good example for learning how to unit test flows. This test file sits in our sample repositories under Advanced/obligation-cordapp and is available in both Kotlin and Java versions.

Setup codes for both versions are shown here:


class IOUTransferFlowTests {
    lateinit var mockNetwork: MockNetwork
    lateinit var a: StartedMockNode
    lateinit var b: StartedMockNode
    lateinit var c: StartedMockNode

    @Before
    fun setup() {
        mockNetwork = MockNetwork(listOf("net.corda.training"),
                notarySpecs = listOf(MockNetworkNotarySpec(CordaX500Name("Notary","London","GB"))))
        a = mockNetwork.createNode(MockNodeParameters())
        b = mockNetwork.createNode(MockNodeParameters())
        c = mockNetwork.createNode(MockNodeParameters())
        val startedNodes = arrayListOf(a, b, c)
        // For real nodes this happens automatically, but we have to manually register the flow for tests
        startedNodes.forEach { it.registerInitiatedFlow(IOUIssueFlowResponder::class.java) }
        startedNodes.forEach { it.registerInitiatedFlow(IOUTransferFlowResponder::class.java) }
        mockNetwork.runNetwork()
    }

    @After
    fun tearDown() {
        mockNetwork.stopNodes()
    }


public class IOUTransferFlowTests {

    private MockNetwork mockNetwork;
    private StartedMockNode a, b, c;

    @Before
    public void setup() {
        MockNetworkParameters mockNetworkParameters = new MockNetworkParameters().withCordappsForAllNodes(
                Arrays.asList(
                        TestCordapp.findCordapp("net.corda.samples.contracts")
                )
        ).withNotarySpecs(Arrays.asList(new MockNetworkNotarySpec(new CordaX500Name("Notary", "London", "GB"))));
        mockNetwork = new MockNetwork(mockNetworkParameters);
        System.out.println(mockNetwork);

        a = mockNetwork.createNode(new MockNodeParameters());
        b = mockNetwork.createNode(new MockNodeParameters());
        c = mockNetwork.createNode(new MockNodeParameters());

        ArrayList<StartedMockNode> startedNodes = new ArrayList<>();
        startedNodes.add(a);
        startedNodes.add(b);
        startedNodes.add(c);

        // For real nodes this happens automatically, but we have to manually register the flow for tests
        startedNodes.forEach(el -> el.registerInitiatedFlow(IOUTransferFlow.Responder.class));
        startedNodes.forEach(el -> el.registerInitiatedFlow(IOUIssueFlow.ResponderFlow.class));
        mockNetwork.runNetwork();
    }

    @After
    public void tearDown() {
        mockNetwork.stopNodes();
    }

We create a mock network in our @Before setup method and create a couple of nodes. We also record the identity of the notary in our test network, which will come in handy later. We also tidy up when we’re done.

Writing a test case

Next, we write a test case:


@Test
fun flowReturnsCorrectlyFormedPartiallySignedTransaction() {
    val lender = a.info.chooseIdentityAndCert().party
    val borrower = b.info.chooseIdentityAndCert().party
    val stx = issueIou(IOUState(10.POUNDS, lender, borrower))
    val inputIou = stx.tx.outputs.single().data as IOUState
    val flow = IOUTransferFlow(inputIou.linearId, c.info.chooseIdentityAndCert().party)
    val future = a.startFlow(flow)
    mockNetwork.runNetwork()
    val ptx = future.getOrThrow()
    // Check the transaction is well formed...
    // One output IOUState, one input state reference and a Transfer command with the right properties.
    assert(ptx.tx.inputs.size == 1)
    assert(ptx.tx.outputs.size == 1)
    assert(ptx.tx.inputs.single() == StateRef(stx.id, 0))
    println("Input state ref: ${ptx.tx.inputs.single()} == ${StateRef(stx.id, 0)}")
    val outputIou = ptx.tx.outputs.single().data as IOUState
    println("Output state: $outputIou")
    val command = ptx.tx.commands.single()
    assert(command.value == IOUContract.Commands.Transfer())
    ptx.verifySignaturesExcept(b.info.chooseIdentityAndCert().party.owningKey, c.info.chooseIdentityAndCert().party.owningKey,
            mockNetwork.defaultNotaryNode.info.legalIdentitiesAndCerts.first().owningKey)
}


@Test
    public void flowReturnsCorrectlyFormedPartiallySignedTransaction() throws Exception {
        Party lender = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty();
        Party borrower = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty();
        SignedTransaction stx = issueIOU(new IOUState(Currencies.DOLLARS(10), lender, borrower));
        IOUState inputIou = (IOUState) stx.getTx().getOutputs().get(0).getData();
        IOUTransferFlow.InitiatorFlow flow = new IOUTransferFlow.InitiatorFlow(inputIou.getLinearId(), c.getInfo().getLegalIdentities().get(0));
        Future<SignedTransaction> future = a.startFlow(flow);

        mockNetwork.runNetwork();

        SignedTransaction ptx = future.get();

        // Check the transaction is well formed...
        // One output IOUState, one input state reference and a Transfer command with the right properties.
        assert (ptx.getTx().getInputs().size() == 1);
        assert (ptx.getTx().getOutputs().size() == 1);
        assert (ptx.getTx().getOutputs().get(0).getData() instanceof IOUState);
        assert (ptx.getTx().getInputs().get(0).equals(new StateRef(stx.getId(), 0)));

        IOUState outputIOU = (IOUState) ptx.getTx().getOutput(0);
        Command command = ptx.getTx().getCommands().get(0);

        assert (command.getValue().equals(new IOUContract.Commands.Transfer()));
        ptx.verifySignaturesExcept(b.getInfo().getLegalIdentities().get(0).getOwningKey(), c.getInfo().getLegalIdentities().get(0).getOwningKey(), mockNetwork.getDefaultNotaryIdentity().getOwningKey());
    }

Writing a test is an intuitive process. You are essentially mimicking what the flow does, composing the transaction, and collecting required signatures. In our example above, we first create the state attributes and package them into a state. Then we start the flow with a mock node. For the verification process, we take the obtained signed transaction and assert the required fields of the transaction as well as that of the input/output states.

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.