Solidity Unit Testing with Remix IDE — A Few Missing Pieces

Yesterday, I made a mockery of myself trying to unit test a Solidity smart control for a lab assignment I was preparing. After several hours of effort finally, I got it. My issues were due to a mix of reasons, such as the way the smart contract was written, not reading minor details on the Remix guide, and lack of explanation on how to apply the example for a more generic case. While I had written and tested decent size programs in several languages, I haven’t used test suits extensively. Following is a reflection of my experience and hope it gives a bit of insight for your unit testing project with Remix IDE.

I was testing a voting smart contract and it had the following properties:

  • No getter functions — I was relying on the compiler to generate getter functions for public variables. These weren’t accessible within unit test code, as remix-test refused to run saying no such function
  • Custom transaction context— Relied on msg.sender and modifier to determine who can do what. Didn’t immediately realize that msg.sender can be set only in inherited contracts
  • Not every function in my contract returned a value making it difficult to know whether something worked
  • I misinterpreted account labels like account-1 with corresponding variables like acc1
  • try-catch didn’t work in inherit test contract
  • There was no way to know what was returned in case of a failure. Compared to some of the other test suites, no received value was indicated

Following is an extract from my smart contract to keep track of a list of accounts that can vote on something:

pragma solidity ^0.6.0;contract VotersList{

struct Voter {
string name;
bool voted;
}
mapping(address => Voter) public voters; //List of voters
uint public numVoters = 0;
address public manager; //Manager of voting contract constructor () public {
manager = msg.sender; //Set contract creator as manager
} //Add new voter
function addVoter(address voterAddress, string memory name) public restricted returns (uint){
Voter memory v;
v.name = name;
v.voted = false;
voters[voterAddress] = v;
numVoters++;
return numVoters;
}

modifier restricted() { //Only manager can do
require (msg.sender == manager);
_;
}
}

As I wanted to test only the manager can call the addVoter function, I had to use the custom transaction context. However, it took a while to realize msg.sender can be set only in an inherited contract. Alternatively, I had to use inheritance anyway, as there were no explicitly defined getter functions to test the behaviour. However, this also means try-catch can’t be used to get a bit of detail about a failure. Following is the corresponding test contract:

pragma solidity >=0.4.22 <0.7.0;
import "remix_tests.sol"; // this import is automatically injected by Remix.
import "./VotersList.sol";
import "remix_accounts.sol"; //Use accounts defined here for testing// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract VoterListTest is VotersList {
address acc0; //Variables used to emulate different accounts
address acc1;
address acc2;
address acc3;/// 'beforeAll' runs before all other tests
function beforeAll() public {
acc0 = TestsAccounts.getAccount(0); //Initiate acc variables
acc1 = TestsAccounts.getAccount(1);
acc2 = TestsAccounts.getAccount(2);
acc3 = TestsAccounts.getAccount(3);
}

/// Account at index zero (account-0) is default account, so manager will be set to acc0
function managerTest() public {
Assert.equal(manager, acc0, 'Manager should be acc0');
}

/// Add a voter as manager
/// When msg.sender isn't specified, default account (i.e., account-0) is considered as the sender
function addVoter() public {
Assert.equal(addVoter(acc1, 'Alice'), 1, 'Should be equal to 1');
}

/// Try to add voter as a user other than manager. This should fail
/// #sender: account-1
function addVoterFailure() public {
Assert.equal(addVoter(acc2, 'Bob'), 2, 'Should be equal to 2');
}

/// Try to add voter as manager again
function addVoter2() public {
Assert.equal(addVoter(acc3, 'Charlie'), 2, 'Should be equal to 2');
}

/// Verify number of votes
function voteOpenTest() public {
Assert.equal(numVoters, 2, 'Should be equal to 2');
}
}

A few additional things to note from the above code is:

  • account-1 is a label for an account that is used to set the msg.sender, while acc0 is the variable. TestsAccounts.getAccount(0) could be directly used too
  • Account at index zero (i.e., account-0) is the default account. If #sender: isn’t set, the default account is assumed
  • addVoterFailure test case will fail as we can’t use try-catch. However, a bit of detail will still appear on Remix

Following is the result of unit testing and the code can be found from my GitHub: