Skip to content
Document

Testing a smart contract

The testing framework allows you to deploy and transact with the contract on a local blockchain. Tests for your smart contract helps you avoid mistakes and exploits in the contract, you are developing. To learn how to write tests for smart contracts, take a look at our example contracts. All of those contracts have tests that ensure 100% code coverage.

After reading this page, you should be able to do the following:

  1. Write a test, that deploys a contract
  2. Use the deployment test as setup for another test.
  3. Interact with the contract in the test.

The tests are written in Java, utilizing our testing framework JUnit-contract-test. During the test we are, provided a blockchain object, which allows us to deploy and interact with multiple different contracts.

How to run our example tests

To run the tests, navigate to the root of example contracts and execute the following commands in order.

Compile the contracts:

cargo pbc build --release

Go into the test directory, compile the tests and run them.

cd java-test
mvn test-compile test

You can also run the provided script run-java-tests.sh, where a build flag, -b, for compiling the contracts before test run, can be given. This would be equivalent to running the above commands in order.

./run-java-tests.sh -b

To run a single test class, use the maven option -Dtest="Name of Test Class", and run it while standing in the test directory.

mvn test -Dtest="VotingTest"

Run test with code coverage

To see that the tests hits the different parts of the smart contract code, we can run the tests with coverage enabled. The tests will generate coverage information, which can be compiled into a report. To run the tests with coverage enabled, execute run-java-tests.sh with the build flag, -b, and the coverage flag, -c.

./run-java-tests.sh -c -b

This will create the instrumented executable, and run the test with coverage enabled. The coverage report will be located in the java-test/target/coverage/html, where the index.html can be opened in your browser.

Detailed explanation of the voting test example

The following section is the JUnit test of our voting smart contract. This example will be explained in the following sections.

import com.partisiablockchain.BlockchainAddress;
import com.partisiablockchain.language.abicodegen.Voting;
import com.partisiablockchain.language.junit.ContractBytes;
import com.partisiablockchain.language.junit.ContractTest;
import com.partisiablockchain.language.junit.JunitContractTest;
import com.partisiablockchain.language.junit.exceptions.ActionFailureException;

import java.nio.file.Path;
import java.util.List;

import org.assertj.core.api.Assertions;

/** Test suite for the Voting contract. */
public final class VotingTest extends JunitContractTest {
    private static final ContractBytes VOTING_CONTRACT_BYTES =
            ContractBytes.fromPaths(
                    Path.of("../target/wasm32-unknown-unknown/release/voting.wasm"),
                    Path.of("../target/wasm32-unknown-unknown/release/voting.abi"),
                    Path.of("../target/wasm32-unknown-unknown/release/voting_runner"));
    private BlockchainAddress voter1; //Unique address of a voter on Partisia Blockchain
    private BlockchainAddress voter2;
    private BlockchainAddress voter3;
    private BlockchainAddress voting;

    /** Setup for all the other tests. Deploys a voting contract and instantiates accounts. */
    @ContractTest
    void setUp() {
        voter1 = blockchain.newAccount(2);
        voter2 = blockchain.newAccount(3);
        voter3 = blockchain.newAccount(4);

        // Initialize a voting with a proposal ID, a list of voters and a deadline.
        byte[] initRpc = Voting.initialize(10, List.of(voter1, voter2, voter3), 60 * 60 * 1000);
        voting = blockchain.deployContract(voter1, VOTING_CONTRACT_BYTES, initRpc);
    }

    /** An eligible voter can cast a vote. */
    @ContractTest(previous = "setUp")
    public void castVote() {
        byte[] votingRpc = Voting.vote(true);
        // An eligible voter casts a vote
        blockchain.sendAction(voter1, voting, votingRpc);
        // Get the current state of the voting contract
        Voting.VoteState state = Voting.VoteState.deserialize(blockchain.getContractState(voting));

        // One vote should be cast, and it should be "true"
        Assertions.assertThat(state.votes().size()).isEqualTo(1);
        Assertions.assertThat(state.votes().get(voter1)).isTrue();
    }
}

Abstract Test Class

The test extends the JunitContractTest class, this is the extension providing the blockchain used in the tests.

JunitContractTest explained:
public final class VotingTest extends JunitContractTest { // The test class must extend to JunitContractTest, to run Smart Contract Tests.
    // Fields and tests.
}

Deploying smart contract bytecode

In order to test a smart contract, you need to deploy the contract. To deploy a contract, we must specify the bytecode to deploy. We have declared an instance of ContractBytes, where we specify the location of the .wasm and .abi file for the example voting contract. The three paths declared are the path to the wasm, the abi and the local instrumented executable. The local instrumented executable is used for code coverage, the executable is named runner after the contract name.

ContractBytes explained:
    private static final ContractBytes VOTING_CONTRACT_BYTES =
      ContractBytes.fromPaths(
              Path.of("../target/wasm32-unknown-unknown/release/voting.wasm"),  // The path to the WASM of the contract.
              Path.of("../target/wasm32-unknown-unknown/release/voting.abi"),   // The path to the ABI of the contract.
              Path.of("../target/wasm32-unknown-unknown/release/voting_runner") // The path to the instrumented executable.
      );

BlockchainAddress Fields - Values transferred

The four fields with type BlockchainAddress, voter1, voter2, voter3 and voting, these will be populated in the test, where a public account is created for each voter, and when deploying the voting contract, the call returns the address the contract was deployed at.

These fields will be transferred to tests depending on the test, if the field is populated at the end of test execution. This means we only have to instantiate the field once. To create an account, we call the blockchain.newAccount(1), where 1 is the secret key for the Address created.

The address for the contract, is created when it is deployed. To deploy a contract, the address sending the transaction is needed, the bytecode to deploy and the arguments for the initialize call, creating the initial state of the contract.

Addresses - By deployment or creation
voter1 = blockchain.newAccount(2); // Create an account with secret key '2' and return the related BlockchainAddress.
voter2 = blockchain.newAccount(3); // Create an account with secret key '3' and return the related BlockchainAddress.
voter3 = blockchain.newAccount(4); // Create an account with secret key '4' and return the related BlockchainAddress.

byte[] initRpc =
    Voting.initialize(10, List.of(voter1, voter2, voter3), 60 * 60 * 1000); // The rpc for the init function when creating a contract.

voting =
    blockchain.deployContract(voter1, VOTING_CONTRACT_BYTES, initRpc); // Deployment of a contract, returns the address the contract is deployed at.

Using ABI codegen for RPC

Calling actions in a contract requires byte streams serialized according to the RPC binary format.

In the deployment call, the RPC is serialized using code generated from the ABI of the contract. This can either be generated with ABI client, or using the Maven plugin.

The generated code provides methods for serializing the RPC for all actions in the contract. The generated code also provides a state deserialization method. The generated code deserializes a state given in bytes, to a record object, which can be used to assert on the state of a contract in a test. An example of the state serialization is in the second test.

State
Voting.VoteState state = // The state variable created from the deserialization.
    Voting.VoteState.deserialize(blockchain.getContractState(voting)); // The call to the generated deserialize state method

Assertions.assertThat(state.votes().size()).isEqualTo(1); // Assertion on the amount of votes currently.
Assertions.assertThat(state.votes().get(voter1)).isTrue(); // Assertion on who voted.

How to build on top of an already existing test

Smart contracts are great since the order of operations are clear, from the perspective of behavioral execution. This means that every time we would have to test 2 different actions, meaning two different behaviors, on the same state. Tests would have to perform all the actions up to that point, to then perform one of the actions, and then perform them all again for the other action.

Here the testing framework helps, the methods with the annotation @ContractTest, can be used as setup for other tests.

@ContractTest and previous
@ContractTest // The annotation with no previous, means the test is independent of other test, also called 'root-test'.
void setUp() {
// Omitted
}

/** An eligible voter can cast a vote. */
@ContractTest(previous = "setUp") // The previous for the test, is the 'setUp', so for the test to succeed the 'setUp' test must be run before this test.
public void castVote() {
// Omitted
}

Sending an Action to a deployed contract.

Interaction with the contract, is made by sending an action to a deployed contract. The action needs three parameters, the address for the contract the action is targeting, the address of the sender and the RPC, which specifies the arguments for the action call.

Send action.
@ContractTest(previous = "setUp")
public void castVote() {
byte[] votingRpc = Voting.vote(true); // Create the RPC with the generated code.
blockchain.sendAction(voter1, voting, votingRpc); // Send the action, with the sender, the target contract and the RPC.
Voting.VoteState state = Voting.VoteState.deserialize(blockchain.getContractState(voting)); // Get the state of the contract and deserialize.

Assertions.assertThat(state.votes().size()).isEqualTo(1); // Assert the vote is registered.
Assertions.assertThat(state.votes().get(voter1)).isTrue(); // Assert the registered vote is from 'voter1'.

}

Second test using the first test as a setup

We can now perform our first interaction with the deployed contract. So we add another test to our test class. Another account must be created, so we add another field, receiver, and create the account.

Notice in the annotation @ContractTest, we have provided an argument that points to our previous method, where we deployed our contract. In our example, that is setup.

In this test, we send an action to our contract, the functionality we are testing is just a normal transfer from owner to receiver. We call the .sendAction(), where we provided the address of the contract, we want to send the action to. The sender of the action, and the RPC for the call. The RPC in this case contains the receiver of the transfer, and the amount to send.

If you want to more examples of testing smart contracts, go to example contracts, where there are multiple tests.