Test and Debug
You can utilize the built in Java testing packages when creating your dApps on the Aion network. They allow you to deploy your Java smart contract and write tests to interact with the contract under a real Aion blockchain environment without actually running an Aion blockchain. In this section, we're going to be walking through the entire workflow of deploying a Java contract to the local kernel, writing some tests, and finally running those tests against your project.
Prerequisites
This guide assumes that you have the IntelliJ IDE installed, along with the Aion4j plugin. You do not need to have an IntelliJ project ready, as we will be creating a new project in this guide.
Set up a Project
We will use IntelliJ to create a new Java dApp project.
Create a project
- With IntelliJ open, go to File → New → Project or click Create New Project from the splash screen.
- Select Maven from the options on the left.
- Check Create from archetype.
- Select
org.aion4j:avm-archetype
from the list and click Next. - Enter the GroupID and ArtifactID for your project. For more information on these values check out the Apache Maven documentation. For this guide we’re going to enter
aion
,example
. - Click Next when you have finished.
- Click Next.
- Click Finish.
- An Import popup will appear at the bottom right of the screen once everything has loaded. Click Enable Auto-Import.
Copy the Contract
We’re going to use the contract below to create our tests. The contract assigns the variable owner
to the address that deploys the contract. This owner is allowed to transfer the ownership of the contract to another account. Ownership will only be transferred when the assigned new owner accepts the ownership. Once the ownership transfer is completed, an OwnershipTransferred
event will be logged along with the old owner address and the new owner address.
- Open the
HelloAvm
file within your project. - Delete the entire contents of the file.
- Copy and paste the contract listed below into the
HelloAvm
file.
import avm.Address;
import avm.Blockchain;
import org.aion.avm.tooling.abi.Callable;
import org.aion.avm.tooling.abi.Initializable;
public class SimpleOwnable {
private static Address owner;
private static Address newOwner;
// Assign the log topic upon deployment.
@Initializable
private static String transferLogTopic;
@Initializable
private static String deployer;
static {
// Assign the deployer as the owner.
owner = Blockchain.getCaller();
newOwner = new Address(new byte[32]);
// Log the deployer upon deployment
Blockchain.log(deployer.getBytes());
}
// Save the address of the new owner, wait for acceptance.
@Callable
public static void transferOwnership(Address newOwnerAddress) {
Blockchain.require(Blockchain.getCaller().equals(owner));
newOwner = newOwnerAddress;
}
// New owner accepts the ownership. Update the owner address and log the transfer.
@Callable
public static void acceptOwnership() {
Blockchain.require(Blockchain.getCaller().equals(newOwner));
Blockchain.log(transferLogTopic.getBytes(), owner.toByteArray(), newOwner.toByteArray());
owner = newOwner;
newOwner = new Address(new byte[32]);
}
// Get the contract owner address.
@Callable
public static Address getOwnerAddress() {
return owner;
}
}
5. Save the file.
Write Tests
Now we are ready to test our contract without deploying the contract to the network. First up we need to create our test file.
- Within the
src/test/java/exmaple
, open theRuleTest
file. - Delete the contents of the file. You should be left with an empty
HelloAvmRuleTest
file.
Next we need to instantiate the AvmRule
class. It creates an in-memory representation of the Aion kernel and AVM. Add the following lines to the top of the HelloAvmRuleTest
.
@Rule
public AvmRule avmRule = new AvmRule(true);
There are two things to point out here:
@Rule
indicates that an embedded AVM will be instantiated for each test method with this file. This means that each individual test will talk to a fresh version of the kernel. If you want all your tests to be run in the same AVM and kernel, change@Rule
to@ClassRule
.AvmRule
takes a boolean argument wheretrue
enables debug mode, giving you more verbose output from IntelliJ.
Next, we need to create an account for the test to use. The avmRule
class comes with a default account and some tokens already in it. Add the following line to the test:
private Address deployer = avmRule.getPreminedAccount();
We should also create a contractAddress
variable. Add the following line into the test:
private Address contractAddress;
We will assign contract in a method with @Before
so that it will be run before the @Test
method.
@Before
public void deployContract() {...}
Then we will get the bytes that represent the contract jar, along with the deployment arguments in byte[] if required.
Use ABIStreamingEnocder to encode the deployment arguments:
byte[] deploymentArguments = encoder.encodeOneString("OwnershipTransferred").encodeOneString("Jennifer").toBytes();
Then, we use avmRule.getDappBytes
to get the contract bytes by providing the main class for the contract(contract entry point), deployment arguments, and any other classes that is needed for the contract jar:
byte[] contract = avmRule.getDappBytes(SimpleOwnable.class, deploymentArguments);
After everything is set, we can deploy the contract and get the contract address:
contractAddress = avmRule.deploy(deployer, BigInteger.ZERO, contract).getDappAddress();
Till now, we have:
public class SimpleOwnableRuleTest {
@Rule
public AvmRule avmRule = new AvmRule(true);
// Default address with balance
private Address deployer = avmRule.getPreminedAccount();
private Address contractAddress;
@Before
public void deployContract() {
ABIStreamingEncoder encoder = new ABIStreamingEncoder();
byte[] deploymentArguments = encoder.encodeOneString("OwnershipTransferred").encodeOneString("Jennifer").toBytes();
byte[] contract = avmRule.getDappBytes(SimpleOwnable.class, deploymentArguments);
// Deploy the contract and get the contract address
contractAddress = avmRule.deploy(deployer, BigInteger.ZERO, contract).getDappAddress();
}
Single Method Test
Now we have a contract deployed in the kernel, we can start to interact with it and test if it works. In this example, we will see if the owner
is set to the deployer account upon deployment by calling getOwnerAddress
.
To do that, we will be using avmRule.call
, which takes the caller address, the contract address, any value that is wanted to the contract along with the call, and the transaction data. The transaction data will first include the name of the method we want to call, then the arguments that are required.
Again, we can use ABIStreamingEncoder:
ABIStreamingEncoder encoder = new ABIStreamingEncoder();
byte[] txData = encoder.encodeOneString("getOwnerAddress").toBytes();
Then, we will make the call and hold the result in an AvmRule.ResultWrapper
:
AvmRule.ResultWrapper result = avmRule.call(deployer, contractAddress, BigInteger.ZERO, txData);
Then we can make sure the transaction call is successful by checking the status
in the receipt:
ResultCode status = result.getReceiptStatus();
Assert.assertTrue(status.isSuccess());
If so, we check the return owner address is the same with the deployer address:
Address res = (Address) result.getDecodedReturnData();
Assert.assertTrue(res.equals(deployer));
The complete test is:
@Test
public void testGetOwnerAddress() {
ABIStreamingEncoder encoder = new ABIStreamingEncoder();
byte[] txData = encoder.encodeOneString("getOwnerAddress").toBytes();
AvmRule.ResultWrapper result = avmRule.call(deployer, contractAddress, BigInteger.ZERO, txData);
ResultCode status = result.getReceiptStatus();
Assert.assertTrue(status.isSuccess());
// Cast the return type
Address res = (Address) result.getDecodedReturnData();
Assert.assertTrue(res.equals(deployer));
}
Then we can run the test:
Great, it passed!
Let’s write another test for transferOwnership
, where the method can be only called by the owner, therefore, we need to make sure that anyone that is not an owner, should fail the call.
Again, we need to encode the transaction data, which is the method name and the arguments. This method requires an Address
, we can use avmRule.getRandomAddress
to get a random address:
ABIStreamingEncoder encoder = new ABIStreamingEncoder();
Address newOwner = avmRule.getRandomAddress(BigInteger.ZERO);
byte[] txData = encoder.encodeOneString("transferOwnership").encodeOneAddress(newOwner).toBytes();
Then, we need a different address with sufficient balance, that is not the deployer(current owner) to make the call, and we are expecting the call to be failed. The complete test is as follows:
@Test
public void testTransferOwnership() {
ABIStreamingEncoder encoder = new ABIStreamingEncoder();
Address newOwner = avmRule.getRandomAddress(BigInteger.ZERO);
byte[] txData = encoder.encodeOneString("transferOwnership").encodeOneAddress(newOwner).toBytes();
Address caller = avmRule.getRandomAddress(BigInteger.TEN.pow(10));
AvmRule.ResultWrapper result = avmRule.call(caller, contractAddress, BigInteger.ZERO, txData);
TransactionStatus status = result.getReceiptStatus();
Assert.assertTrue(status.isFailed());
}
Then if we run the test:
The output shows that the test is passed because the call is failed as expected. Since we turned on the debug mode, we can see that the transaction is being reverted due to a revert exception in the AVM caused by avm_require
:
i.RevertException
at org.aion.avm.core.BlockchainRuntimeImpl.avm_require(BlockchainRuntimeImpl.java:413)
at p.avm.Blockchain.avm_require(Blockchain.java:228)
at SimpleOwnable.avm_transferOwnership(Unknown Source)
at SimpleOwnable.avm_main(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.aion.avm.core.persistence.LoadedDApp.callMain(LoadedDApp.java:237)
at org.aion.avm.core.DAppExecutor.call(DAppExecutor.java:62)
at org.aion.avm.core.AvmImpl.commonInvoke(AvmImpl.java:411)
at org.aion.avm.core.AvmImpl.runExternalInvoke(AvmImpl.java:329)
at org.aion.avm.core.AvmImpl.backgroundProcessTransaction(AvmImpl.java:233)
at org.aion.avm.core.AvmImpl.access$300(AvmImpl.java:35)
at org.aion.avm.core.AvmImpl$AvmExecutorThread.run(AvmImpl.java:100)
Breakpoint and Debug
We can set breakpoints in the contract for debugging purposes, or we can just use it as a pause, and monitor the transaction call execution.
We will use testTransferOwnership
as an example. Let’s see if the transaction is actually failed because the caller is not the current owner.
We first need to set the breakpoint in the contract:
Then, we can debug
the testTransferOwnership
:
We can see that transferOwnership
is being called, and Blockchain.require
is being checked and it fails since the caller address does not match the owner address.