Skip to content

Commit

Permalink
CORE-16815: added explanatory comments for contract testing in apples
Browse files Browse the repository at this point in the history
  • Loading branch information
christian-napoles-r3 committed Aug 31, 2023
1 parent 70a25b1 commit d8db1fd
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,62 @@ import com.r3.developers.apples.states.AppleStamp
import net.corda.v5.ledger.utxo.Command
import org.junit.jupiter.api.Test

/**
* This class is an implementation of the ApplesContractTest which implements the ContractTest abstract class.
* The ContractTest class provides functions to easily perform unit tests on contracts.
* The ApplesContractTest adds additional default values for states as well as helper functions to make utxoLedgerTransactions.
* This allows us to test our implementation of contracts without having to trigger a workflow.
**/

// This specific class is involved with testing the scenarios involving the AppleStamp state and the AppleCommands.Issue command.
class AppleStampContractIssueCommandTest : ApplesContractTest() {

/**
* All Tests must start with the @Test annotation. Tests can be run individually by running them with your IDE.
* Alternatively, tests can be grouped up and tested by running the test from the line defining the class above.
* If you need help to write tests, think of a happy path scenario and then think of every line of code in the contract
* where the transaction could fail.
* It helps to meaningfully name tests so that you know exactly what success case or specific error you are testing for.
**/

@Test
fun happyPath() {
// The following test builds a transaction that should pass all the contract verification checks.
// The buildTransaction function helps create a utxoLedgerTransaction that can be referenced for contract tests
val transaction = buildTransaction {
addCommand(AppleCommands.Issue())
addOutputState(outputAppleStampState)
addSignatories(outputAppleStampStateParticipants)
}
/**
* The assetVerifies function is the general way to test if a contract test passes or fails a transaction.
* Contract test verifications occurs under the hood whenever we build a transaction.
* Therefore, if a transaction is verified, by extension the contract tests pass.
**/
assertVerifies(transaction)
}

@Test
fun outputContractStateSizeNotOne() {
// The following test builds a transaction that would fail due to not meeting the expectation that a transaction
// in this CorDapp for this state should only contain one output state.
val transaction = buildTransaction {
addCommand(AppleCommands.Issue())
addOutputState(outputAppleStampState)
addOutputState(outputAppleStampState)
addSignatories(outputAppleStampStateParticipants)
}
/**
* The assertFailsWith function is the general way to test for unhappy path test cases contract tests.
*
* The transaction defined above will fail because the contract expects only one output state. However, we built
* a transaction with two output states. So we expect the transaction to fail, and only 'pass' our test if we \
* can match the error message we expect.
*
* NOTE: the assertFailsWith method tests if the exact string of the error message matches the expected message
* to test if the string of the error message contains a substring within the error message, use the
* assertFailsWithMessageContaining() function using the same arguments.
**/
assertFailsWith(
transaction,
"This transaction should only have one AppleStamp state as output"
Expand All @@ -33,9 +69,12 @@ class AppleStampContractIssueCommandTest : ApplesContractTest() {

@Test
fun blankStampDescription() {
// The following test builds a transaction that would fail due to having an empty stamp description string
val transaction = buildTransaction {
addCommand(AppleCommands.Issue())
addOutputState(
// Sometimes the default values defined in AppleContractTest does not cover all scenarios.
// Specific setups are recommended to be written individually in tests unless they are repeatable.
AppleStamp(
outputAppleStampStateId,
"",
Expand All @@ -54,6 +93,7 @@ class AppleStampContractIssueCommandTest : ApplesContractTest() {

@Test
fun missingCommand() {
// The following test builds a transaction that would fail due to not having a command.
val transaction = buildTransaction {
addOutputState(outputAppleStampState)
addSignatories(outputAppleStampStateParticipants)
Expand All @@ -63,6 +103,7 @@ class AppleStampContractIssueCommandTest : ApplesContractTest() {

@Test
fun unknownCommand() {
// The following test builds a transaction that would fail due to providing an invalid command.
class MyDummyCommand : Command;

val transaction = buildTransaction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,32 @@ import net.corda.v5.ledger.utxo.StateAndRef
import java.security.PublicKey
import java.util.UUID

/**
* The following is the base implementation of the Contract Tests for the Apples CSDE-template-tutorial.
*
* - The AppleContractTest abstract class implements the ContractTest class.
* - For full contract test coverage, we generally create a class for every command scenario for every state.
* - Each of these classes will implement the abstract class to incorporate ContractTest general testing functionality
* as well as the functionality specific for this CordApp tutorial example.
*
* In this case, we have 3 scenarios (you can refer to contract files for the apples tutorial):
* 1. AppleStamp state, AppleCommands.Issue command
* 2. BasketOfApples state, AppleCommand.PackBasket command
* 3. BasketOfApples state, AppleCommand.Redeem command
*
* The variables and methods within this abstract class should to written to enable the re-use of code such that states
* are set up automatically so that you only need to worry about the logic of your contracts
**/

@Suppress("UnnecessaryAbstractClass", "UtilityClassWithPublicConstructor")
abstract class ApplesContractTest : ContractTest() {

/**
* The following are implementations of default values for the AppleStamp state for contract testing.
* Some values such as the public keys already have default values implemented by the ContractTest class.
* for more information, navigate to their declaration
*/
// Default values for AppleStamp state
protected val outputAppleStampStateId: UUID = UUID.randomUUID()
protected val outputAppleStampStateStampDesc: String = "Can be exchanged for a single basket of apples"
protected val outputAppleStampStateIssuer: PublicKey = bobKey
Expand All @@ -24,6 +47,7 @@ abstract class ApplesContractTest : ContractTest() {
outputAppleStampStateParticipants
)

// Default values for BasketOfApples state
protected val outputBasketOfApplesStateDescription: String = "Golden delicious apples, picked on 11th May 2023"
protected val outputBasketOfApplesStateFarm: PublicKey = bobKey
protected val outputBasketOfApplesStateOwner: PublicKey = bobKey
Expand All @@ -37,6 +61,9 @@ abstract class ApplesContractTest : ContractTest() {
outputBasketOfApplesStateParticipants
)

// Helper function to create input AppleStamp state when building a transaction for contract testing.
// The argument for outputState for this and the next helper function is set to the default apple state for happy path scenarios.
// To capture negative test cases or other edge cases, they are written within individual tests.
protected fun createInputStateAppleStamp(outputState: AppleStamp = outputAppleStampState): StateAndRef<AppleStamp> {
val transaction = buildTransaction {
addOutputState(outputState)
Expand All @@ -46,6 +73,7 @@ abstract class ApplesContractTest : ContractTest() {
return transaction.toLedgerTransaction().getOutputStateAndRefs(AppleStamp::class.java).single()
}

// Helper function to create input BasketOfApples state when building a transaction for contract testing.
protected fun createInputStateBasketOfApples(outputState: BasketOfApples = outputBasketOfApplesState): StateAndRef<BasketOfApples> {
val transaction = buildTransaction {
addOutputState(outputState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,73 @@ import com.r3.developers.apples.states.BasketOfApples
import net.corda.v5.ledger.utxo.Command
import org.junit.jupiter.api.Test

/**
* This class is an implementation of the ApplesContractTest which implements the ContractTest abstract class.
* The ContractTest class provides functions to easily perform unit tests on contracts.
* The ApplesContractTest adds additional default values for states as well as helper functions to make utxoLedgerTransactions.
* This allows us to test our implementation of contracts without having to trigger a workflow.
**/

// This specific class is involved with testing the scenarios involving the BasketOfApples state and the AppleCommands.PackBasket command.
class BasketOfApplesContractPackBasketCommandTest : ApplesContractTest() {

/**
* All Tests must start with the @Test annotation. Tests can be run individually by running them with your IDE.
* Alternatively, tests can be grouped up and tested by running the test from the line defining the class above.
* If you need help to write tests, think of a happy path scenario and then think of every line of code in the contract
* where the transaction could fail.
* It helps to meaningfully name tests so that you know exactly what success case or specific error you are testing for.
**/

@Test
fun happyPath() {
// The following test builds a transaction that should pass all the contract verification checks.
// The buildTransaction function helps create a utxoLedgerTransaction that can be referenced for contract tests.
val transaction = buildTransaction {
addCommand(AppleCommands.PackBasket())
addOutputState(outputBasketOfApplesState)
addSignatories(outputBasketOfApplesStateParticipants)
}
/**
* The assetVerifies function is the general way to test if a contract test passes or fails a transaction.
* Contract test verifications occurs under the hood whenever we build a transaction.
* Therefore, if a transaction is verified, by extension the contract tests pass.
**/
assertVerifies(transaction)
}

@Test
fun outputContractStateSizeNotOne() {
// The following test builds a transaction that would fail due to not meeting the expectation that a transaction
// in this CorDapp for this state should only contain one output state.
val transaction = buildTransaction {
addCommand(AppleCommands.PackBasket())
addOutputState(outputBasketOfApplesState)
addOutputState(outputBasketOfApplesState)
addSignatories(outputBasketOfApplesStateParticipants)
}
/**
* The assertFailsWith function is the general way to test for unhappy path test cases contract tests.
*
* The transaction defined above will fail because the contract expects only one output state. However, we built
* a transaction with two output states. So we expect the transaction to fail, and only 'pass' our test if we \
* can match the error message we expect.
*
* NOTE: the assertFailsWith method tests if the exact string of the error message matches the expected message
* to test if the string of the error message contains a substring within the error message, use the
* assertFailsWithMessageContaining() function using the same arguments.
**/
assertFailsWith(transaction, "This transaction should only output one BasketOfApples state")
}

@Test
fun blankDescription() {
// The following test builds a transaction that would fail due to having an empty stamp description string.
val transaction = buildTransaction {
addCommand(AppleCommands.PackBasket())
addOutputState(
// Sometimes the default values defined in AppleContractTest does not cover all scenarios.
// Specific setups are recommended to be written individually in tests unless they are repeatable.
BasketOfApples(
"",
outputBasketOfApplesStateFarm,
Expand All @@ -48,6 +87,7 @@ class BasketOfApplesContractPackBasketCommandTest : ApplesContractTest() {

@Test
fun basketWeightIsZero() {
// The following test builds a transaction that would fail due to having a weight not greater than 0
val transaction = buildTransaction {
addCommand(AppleCommands.PackBasket())
addOutputState(
Expand All @@ -66,6 +106,7 @@ class BasketOfApplesContractPackBasketCommandTest : ApplesContractTest() {

@Test
fun basketWeightIsNegative() {
// The following test builds a transaction that would fail due to having a weight not greater than 0
val transaction = buildTransaction {
addCommand(AppleCommands.PackBasket())
addOutputState(
Expand All @@ -84,6 +125,7 @@ class BasketOfApplesContractPackBasketCommandTest : ApplesContractTest() {

@Test
fun missingCommand() {
// The following test builds a transaction that would fail due to not having a command.
val transaction = buildTransaction {
addOutputState(outputBasketOfApplesState)
addSignatories(outputBasketOfApplesStateParticipants)
Expand All @@ -93,6 +135,7 @@ class BasketOfApplesContractPackBasketCommandTest : ApplesContractTest() {

@Test
fun unknownCommand() {
// The following test builds a transaction that would fail due to providing an invalid command.
class MyDummyCommand : Command

val transaction = buildTransaction {
Expand Down
Loading

0 comments on commit d8db1fd

Please sign in to comment.