Java开发的智能合约单元测试教程

在本教程中,我们将实现一个简单的合约,为其编写单元测试,并运行调试过程以查找Bug。

运行该示例所需的全部是Java和IDE。 只需创建一个依赖AVM最新工具库的Java项目作为库。

1.编写智能合约





首先创建智能合约。以下是可用于简单投票DApp的智能合约的示例。

智能合约的功能:

1. 只有合约所有者(部署合约的帐户)可以添加或删除成员。


2. 只有会员可以介绍新的提案。


3. 只有会员可以对提案进行投票。


4. 如果超过50%的成员赞成该提案并对其投票,该提案将通过。

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;


        }


    }


}

合约中的static块在部署时仅执行一次。 我们在此块中设置初始成员,minimumQuorum和所有者。尽管我们与一组成员启动了合约,但所有者随后也可以添加和删除成员。

我们使用AionSet和AionMap跟踪成员及其提案。可以使用其唯一标识符从地图访问每个建议。

智能合约的主要职能是:

· addProposal,允许成员添加提案说明以进行投票。


· vote,允许成员通过传递其ID对可用提案进行投票。赢得多数成员投票的提案将通过。请注意,将生成ProposalPassed事件以记录已通过投标的ID。

2.编写单元测试

我们将使用AvmRule编写测试。 AvmRule是用于在嵌入式AVM上测试合约的Junit规则。 它创建Aion内核和AVM的内存中表示形式。每次我们运行测试时,都会刷新内置的内核和AVM实例。

在测试我们的合约之前,我们需要将其部署到内存中的Aion区块链中,并且我们将使用AvmRule完成此任务。

A.实例化AvmRule

您可以看到该规则包含一个布尔参数,该布尔参数启用/禁用调试模式。 最好在启用调试器的情况下编写测试。您可以在下一部分中查看如何调试合约。

@Rule


public AvmRule avmRule = new AvmRule(true);

注意:此行将为每种测试方法实例化嵌入式AVM。如果将规则定义为@classRule,则将仅为测试类实例化AVM和内核的一个实例。

B.获取合约字节

现在,我们必须获取与合约jar的内存表示相对应的字节。为了获取字节,我们将使用AvmRule中的getDappBytes方法。

getDappBytes采用以下参数:

1. 合约的主要类别。


2. 合约构造函数参数,可以在静态块中对其进行访问和解码。


3. DApp jar中需要包含其他类。

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


Class<?>… otherClasses)

C.部署您的智能合约

使用部署功能可以轻松完成智能合约的部署。

public ResultWrapper deploy(Address from, BigInteger value, 


byte[] dappBytes)

AvmRule还提供了在Aion内核中创建具有初始余额的帐户的功能。

以下是由3名成员组成的小组部署投票智能合约的方法。

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.调用方法

我们可以通过以下方式调用合同中的方法:

1. 编码方法名称及其参数。


2. 将编码后的字节传递给AvmRule的call方法。

public ResultWrapper call(Address from, Address dappAddress, 


BigInteger value, byte[] transactionData)

例如我们创建一个新的提案。我们将通过检查是否生成了NewProposalAdded事件以及事件主题和数据正确来验证提案。

@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());


    }

现在我们将提交一个提案以及两个投票。由于有两个不同的成员以ID 0对提案进行了投票,提案应通过。因此我们希望为最后一个事务生成两个不同的事件-Voted和ProposalPassed。

您还可以通过提案ID查询提案的状态。您会看到返回的解码数据为true,表示提案已通过。

@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.调试智能合约

调试我们的智能合约非常容易,只需在源代码中设置断点即可!由于我们在启用调试的情况下创建了AvmRule,因此在达到断点时将停止执行。让我们来看一个例子。

这是部署后智能合约的状态。

Java开发的智能合约单元测试教程

您可以看到该智能合约具有:

1. 3名成员。
2. 0个提案。
3. minimumQuorum = 2。

您也可以检查每个集合的内容。例如通过调用addProposal,您将能够看到更新的AionMap。

让我们实际对调试器进行测试。我们将故意在评估提案通过方式时造成一个简单的错误。我将修改提案通过条件,如下所示。 请注意,等于条件已更改为小于或等于。

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

现在当第一个所有者提交投票时,提案将通过。

让我们调试方法调用并逐步执行该函数。


Java开发的智能合约单元测试教程

您可以看到,尽管minimumQuorum等于2,但是该提案的投票者计数仅为1。我们修改了if语句(来自上面),并且第51行的isPassed标志设置为true。

从那里,您可以轻松地确定错误在代码中的位置。

结论

如果您曾经为以太坊开发过智能合约,就会知道用一种陌生的专用语言编写合约并对其进行调试的痛苦。

任何熟悉Java的人都会感觉像在家中一样使用AVM编写智能合约。另外市场上任何IDE中的所有调试功能都可用于测试和调试Java智能合约。

关键词: Java  智能合约  

该内容来自于互联网公开内容,非区块链原创内容,如若转载,请注明出处:https://htzkw.com/archives/28282

联系我们

aliyinhang@gmail.com