Tutorial on smart contract unit testing developed in Java

1 140
Avatar for hasson
Written by
4 years ago

All you need to run the example is Java and IDE. Just create a Java project that relies on AVM's latest tool library as a library.

1. Writing smart contracts

First create a smart contract. Here is an example of a smart contract that can be used to simply vote for a DApp.

Smart contract functions:

1. Only the contract owner (the account that deployed the contract) can add or delete members.
2. Only members can introduce new proposals.
3. Only members can vote on proposals.
4. If more than 50% of the members approve and vote on the proposal, the proposal will be passed.

package org.aion.avm.embed.temp;

import avm.Blockchain;
import org.aion.avm.tooling.abi.Callable;
import org.aion.avm.userlib.AionMap;
import org.aion.avm.userlib.AionSet ;
import avm.Address;
import org.aion.avm.userlib.abi.ABIDecoder;

public class Voting {

    private static AionSet <Address> members = new AionSet <> ();
    private static AionMap <Integer, 

Proposal> proposals = new AionMap <> ();
    private static Address owner;
    private static int minimumQuorum;

    static {
        ABIDecoder decoder = new ABIDecoder(Blockchain.getData());

        Address[] arg = decoder.decodeOneAddressArray();
        for (Address addr : arg) {
            members.add(addr);
        }
        updateQuorum();
        owner = Blockchain.getCaller();
    }

    @Callable
    public static void addProposal(String description) {
        Address caller = Blockchain.getCaller();
        Blockchain.require(isMember(caller));

        Proposal proposal = new Proposal(description, caller);
        int proposalId = proposals.size();
        proposals.put(proposalId, proposal);

        Blockchain.log("NewProposalAdded".getBytes(), 

Integer.toString(proposalId).getBytes(), caller.toByteArray(), 

description.getBytes());
    }

    @Callable
    public static void vote(int proposalId) {
        Address caller = Blockchain.getCaller();
        Blockchain.require(isMember(caller) && proposals.containsKey(

proposalId));

        Proposal votedProposal = proposals.get(proposalId);
        votedProposal.voters.add(caller);

        Blockchain.log("Voted".getBytes(), 

Integer.toString(proposalId).getBytes(), caller.toByteArray());

        if (!votedProposal.isPassed && votedProposal.voters.size() == 

minimumQuorum) {
            votedProposal.isPassed = true;
            Blockchain.log("ProposalPassed".getBytes(), 

Integer.toString(proposalId).getBytes());
        }
    }

    @Callable
    public static void addMember(Address newMember) {
        onlyOwner();
        members.add(newMember);
        updateQuorum();
        Blockchain.log("MemberAdded".getBytes(), 

newMember.toByteArray());
    }

    @Callable
    public static void removeMember(Address member) {
        onlyOwner();
        members.remove(member);
        updateQuorum();
        Blockchain.log("MemberRemoved".getBytes(), 

member.toByteArray());
    }

    @Callable
    public static String getProposalDescription(int proposalId) {
        return proposals.containsKey(proposalId) ? 

proposals.get(proposalId).description : null;
    }

    @Callable
    public static Address getProposalOwner(int proposalId) {
        return proposals.containsKey(proposalId) ? 

proposals.get(proposalId).owner : null;
    }

    @Callable
    public static boolean hasProposalPassed(int proposalId) {
        return proposals.containsKey(proposalId) && 

proposals.get(proposalId).isPassed;
    }

    @Callable
    public static int getMinimumQuorum() {
        return minimumQuorum;
    }

    @Callable
    public static boolean isMember(Address address) {
        return members.contains(address);
    }

    private static void onlyOwner() {
        Blockchain.require(owner.equals(Blockchain.getCaller()));
    }

    private static void updateQuorum() {
        minimumQuorum = members.size() / 2 + 1;
    }

    private static class Proposal {
        String description;
        Address owner;
        boolean isPassed;
        AionSet<Address> voters = new AionSet<>();

        Proposal(String description, Address owner) {
            this.description = description;
            this.owner = owner;
        }
    }
}

The static block in the contract is executed only once when deployed. We set initial members, minimumQuorum and owner in this block. Although we started a contract with a group of members, the owner can also add and remove members later.

We use AionSet and AionMap to track members and their proposals. Each suggestion can be accessed from the map using its unique identifier.

The main functions of smart contracts are:

· addProposal, which allows members to add proposal descriptions to vote.
· Vote, which allows members to vote on available proposals by passing their ID. The proposal that won the majority vote will be passed. Note that a ProposalPassed event will be generated to record the ID of the bid that has passed.

2. Writing unit tests

We will write tests using AvmRule. AvmRule is a Junit rule for testing contracts on an embedded AVM. It creates in-memory representations of the Aion kernel and AVM. Every time we run the test, the built-in kernel and AVM instances are refreshed.

Before testing our contract, we need to deploy it to the Aion blockchain in memory, and we will use AvmRule for this task.

A. Instantiate AvmRule

You can see that the rule contains a boolean parameter that enables / disables debug mode. It is best to write tests with the debugger enabled. You can see how to debug the contract in the next section.

@Rule
public AvmRule avmRule = new AvmRule (true);

Note: This line will instantiate the embedded AVM for each test method. If the rule is defined as @classRule, only one instance of the AVM and kernel will be instantiated for the test class.

B. Get contract bytes

Now we have to get the bytes corresponding to the memory representation of the contract jar. To get the bytes, we will use the getDappBytes method in AvmRule.

getDappBytes takes the following parameters:

1. The main category of the contract.
2. Contract constructor parameters, which can be accessed and decoded in static blocks.
3. The DApp jar needs to contain other classes.

public byte [] getDappBytes (Class <?> mainClass, byte [] arguments, 
Class <?> ... otherClasses)

C. Deploy your smart contract

Use the deployment function to easily complete the deployment of smart contracts.

public ResultWrapper deploy (Address from, BigInteger value, 
byte [] dappBytes)

AvmRule also provides the ability to create an account with an initial balance in the Aion kernel.

Here's how a three-member team deploys voting smart contracts.

public class VotingContractTest {

    @Rule
    public AvmRule avmRule = new AvmRule (true);

    public Address dappAddress;
    public Address owner = avmRule.getPreminedAccount ();
    public Address[] members = new Address[3];

    @Before
    public void setup() {
        for (int i = 0; i < members.length; i++) {
            // create accounts with initial balance
            members[i] = avmRule.getRandomAddress(

BigInteger.valueOf(10_000_000L));
        }

        // encode members array as an argument
        byte[] deployArgument = ABIUtil.encodeOneObject(members);
        // get the bytes representing the in memory jar
        byte[] dappBytes = avmRule.getDappBytes(

Voting.class, deployArgument);
        //deploy and get the contract address
        dappAddress = avmRule.deploy(

owner, BigInteger.ZERO, dappBytes) .getDappAddress ();
    }
}

D. Calling method

We can call the method in the contract in the following ways:

1. Encode the method name and its parameters.
2. Pass the encoded bytes to the call method of AvmRule.

public ResultWrapper call (Address from, Address dappAddress, 
BigInteger value, byte [] transactionData)

For example, we create a new proposal. We will verify the proposal by checking if the NewProposalAdded event is generated and the event subject and data are correct.

@Test
    public void addProposalTest () {
        String description = "new proposal description";
        byte [] txData = ABIUtil.encodeMethodArguments (

        "addProposal", description);
        AvmRule.ResultWrapper result = avmRule.call (

members [0], dappAddress, BigInteger .ZERO, txData);
        // assert the transaction was successful
        Assert.assertTrue(result.getReceiptStatus().isSuccess());

        // assert the event is generated
        Assert.assertEquals(1, result.getLogs().size());
        Log log = result.getLogs().get(0);

        // validate the topics and data
        Assert.assertArrayEquals(LogSizeUtils.truncatePadTopic(

        "NewProposalAdded".getBytes()), log.copyOfTopics().get(0));
        Assert.assertArrayEquals(LogSizeUtils.truncatePadTopic(

Integer.toString(0).getBytes()), log.copyOfTopics().get(1));
        Assert.assertArrayEquals(LogSizeUtils.truncatePadTopic(

members[0].toByteArray()), log.copyOfTopics().get(2));
        Assert.assertArrayEquals(description.getBytes(), log.copyOfData());
    }

Now we will submit one proposal and two votes. Since two different members voted on the proposal with ID 0, the proposal should be approved. So we want to generate two different events for the last transaction-Voted and ProposalPassed.

You can also check the status of a proposal by proposal ID. You will see that the decoded data returned is true, indicating that the proposal has passed.

@Test
    public void voteTest () {
        String description = "new proposal description";
        byte [] txData = ABIUtil.encodeMethodArguments (

        "addProposal", description);
        AvmRule.ResultWrapper result = avmRule.call (

members [0], dappAddress, BigInteger .ZERO, txData);
        Assert.assertTrue (result.getReceiptStatus (). IsSuccess ());
        Assert.assertEquals (1, result.getLogs (). Size ());
        // vote # 1
        txData = ABIUtil.encodeMethodArguments (" vote ", 0);
        result = avmRule.call (

members[1], dappAddress, BigInteger.ZERO, txData);
        Assert.assertTrue(result.getReceiptStatus().isSuccess());
        Assert.assertEquals(1, result.getLogs().size());
        //vote #2
        txData = ABIUtil.encodeMethodArguments("vote", 0);
        result = avmRule.call(

members[2], dappAddress, BigInteger.ZERO, txData);
        Assert.assertTrue(result.getReceiptStatus().isSuccess());
        Assert.assertEquals(2, result.getLogs().size());
        //validate that the proposal is stored as passed
        txData = ABIUtil.encodeMethodArguments("hasProposalPassed", 0);
        result = avmRule.call(

members[2], dappAddress, BigInteger.ZERO, txData);
        // decode the return data as boolean
        Assert.assertTrue ((boolean) result.getDecodedReturnData ());
    }

3. Debugging the smart contract

Debugging our smart contract is very easy, just set a breakpoint in the source code! Since we created AvmRule with debugging enabled, execution will stop when the breakpoint is reached. Let's look at an example.

This is the state of the smart contract after deployment.

You can see that the smart contract has:

1. 3 members.
2. 0 proposals.
3. minimumQuorum = 2.

You can also check the contents of each collection. For example by calling addProposal, you will be able to see the updated AionMap.

Let's actually test the debugger. We will deliberately make a simple mistake in evaluating how the proposal will be passed. I will modify the conditions for the proposal to pass as shown below. Notice that the equality condition has changed to less than or equal to.

if (! votedProposal.isPassed && votedProposal.voters.size () 
<= minimumQuorum)

Now when the first owner submits a vote, the proposal will pass.

Let's debug the method call and step through the function.

You can see that although minimumQuorum is equal to 2, the voter count for this proposal is only 1. We modified the if statement (from above) and the isPassed flag on line 51 was set to true.

From there, you can easily determine where the error is in your code.

Conclusion

If you have ever developed smart contracts for Ethereum, you will know the pain of writing contracts and debugging them in a strange, proprietary language.

Anyone familiar with Java will feel like using AVM to write smart contracts at home. In addition, all debugging functions in any IDE on the market can be used to test and debug Java smart contracts.

3
$ 1.10
$ 1.00 from @Read.Cash
+ 1
Sponsors of hasson
empty
empty
empty
Avatar for hasson
Written by
4 years ago

Comments

Awesome!

You should use triple backticks to create the code block, see here: https://imgur.com/a/Tcx5vKs

$ 0.00
4 years ago