/
Launch Online IDE

Explore unit tests

A walk through example tests


Reading Time: 8 min

You have already walked through the cordapp-example code and how to run it. Now you can go through the unit tests provided.

In this section, you will:

  • Test the contract.
  • Dig a little deeper into the tests.
  • Test the flow.

Tests on the contract

warn icon

Your contract is what preserves your ledger layer's integrity. It's very important, so your tests should be thorough.

The first thing that you will probably notice, is that function names can be funky in Kotlin. This is a practice accepted only in tests. In Java function names are usually tame by comparison. JUnit5 introduces the concept of @DisplayName, which helps achieve similar results. Exercise caution, as JUnit5 has not yet been thoroughly tested with Corda.

The typical contract unit test:

  1. Builds a transaction.
  2. Then asks the contract(s) to verify it.
  3. Then confirms the expectation was met.

1 and 2 is a good approximation of what happens in real life as a Corda node receives a transaction and verifies it before recording it.

Notice these 2 commands:

They encapsulate 2 things:

  1. Run .verify for all contracts in the transaction.
  2. Confirm the expectation was met. You guessed it:
    1. With fails(), the expectation is that it fails.
    2. With verifies(), the expectation is that it passes.

fails and verifies do not modify the transaction they are testing, so you can add more than 1 call in a single test, conveniently confirming which element was required. If you want to be more strict with your tests (and you should), you may want to add a string to the fails assertion, with failsWith, or 'fails with'. In particular, if you replace the first fails with a failsWith, you will notice that the error message is A transaction must contain at least one command. This message does not come from the contract, it comes from the Corda framework. In other words, this fails is testing the Corda framework instead of the IOUContract contract. Testing the framework should have no place in your unit tests; perhaps in a separate set of tests that help you learn about the framework.

Ok, testing the text message of the error is not as good as testing the exception type, but unfortunately, all these requireThat throw IllegalArgumentException, so this is the best you can do with that.

Wait, doesn't a transaction require a notary too? Yes, it was created by default here.

Digging a bit

Speaking of requireThat and NodeTestUtils.kt, you will recall that it is part of a DSL for contracts. Here too, you have a number of DSLs, this time for the ledger and transactions. Digging a little bit deeper; the following is nice-to-know, not a need-to-know.

ledgerServices is a mocked ServiceHub, the "access-everything-internal interface" that you remember from the flows. Now, ledger(ledgerServices in Java or ledgerServices.ledger in Kotlin is not a function of the ServiceHub or MockServices. And yet it is possible to access it as such because as you have seen, in Kotlin you can define a function receiver, which in this case is defined here. Also, notice the @file:JvmName annotation to make this function pretend it is a static member of a class, and so be usable in tests in Java.

A quick note here. If you look into NodeTestUtils.transaction, you see that it then calls NodeTestUtils.ledger. This means that since your contract tests add a single transaction to the test ledger, you could simplify like so:

public void transactionMustIncludeCreateCommand() {
    transaction(ledgerServices, tx -> {
        tx.output(IOUContract.ID, [...]

You will recall that in Kotlin, when the last parameter of a function is a lambda function, this function can be taken out of the (). This is why you see no () in Kotlin and you see them in Java. Not only that, if you look carefully, you will see that the lambda function named script is declared to have LedgerDSL as its receiver. Thanks to its receiver, this means that inside this lambda function, you have access to all the functions of LedgerDSL, such as transaction without any this. or ledger., as you would need in Java.

If this is not already the case, you can press Alt-Enter to get IntelliJ's contextual suggestions:

IntelliJ's contextual suggestions
IntelliJ's contextual suggestions

After which it hints at this:

IntelliJ's hints of this
IntelliJ's hints of this

The same tricks apply to transaction and its "inner" part output.

So there you have it, Kotlin syntactic sugar helps us build transactions succinctly in what seems to be like a DSL (Domain Specific Language).

Notice another syntactic help courtesy of Kotlin, using what is called Implementation by Delegation. Here, LedgerDSL delegates its implementation of LedgerDSLInterpreter<TransactionDSLInterpreter> to the constructor argument named interpreter. Kotlin dispenses with the boiler plate that would have you reimplement all the LedgerDSLInterpreter functions just to wrap the interpreter's functions.

Let's step out of the rabbit hole.

Tests on the flow

Setup

You learned earlier that flows can be checkpointed to disk. Checkpointing to disk is not something that Java does on its own. In this case, this is done with the help of Quasar's Java agent. In a nutshell, what a Java agent does is run before your code and, most likely, modify your code.

A deployed Corda system is already configured to run Quasar's agent. However, with tests, you rely on IntelliJ, on Gradle, or on your CI server, to run a JVM separate from any knowledge of how Corda operates.

If you configured IntelliJ to use Gradle for build and test, as advised in a previous chapter, then Gradle takes care of applying the Quasar agent before running the tests.

Walk-through

After that, when you review the content of the tests, it is easy to discern the intent. The setup mocks a Corda network, creates test parties, registers flows and flushes the network of all messages once.

Then the first flowRejectsInvalidIOUs test initiates a flow and once again flushes the network. Here, you may want to make it easier for the test to not pass. You expect a failure immediately because the error will be detected in the initiating flow. So you could replace network.runNetwork() with network.runNetwork(1).

On the other hand, for the second test, signedTransactionReturnedByTheFlowIsSignedByTheInitiator , you would need to give network.runNetwork(7) for the test not to time out. This gives you an idea of the back and forth going on here.

Quickly have a look too at the use of Kotlin's lateinit modifier, which lets you declare a variable without initial value, when you know you are going to define it before it is first used; and fail hard if you omitted it. And note the use of node.transaction to wrap some actions on the SQL database and avoid this kind of error:

java.lang.IllegalStateException: Was not expecting to find existing database transaction on current strand when setting database: Thread[main,5,main], net.corda.nodeapi.internal.persistence.DatabaseTransaction@10874b79

The rest of the tests, you shall find out:

  • Check that the flow created:
    • The intended transaction.
    • The intended output.
    • The intended signatures.
  • Check that the flow saved it all into the vault.

Conclusion

You now know what tests on a contract and on a flow look like, and what they test. Perhaps you learned a bit about Kotlin's syntax.

In the next chapter, you will step away from the code and think about design.

Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!