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
andmodifier
to determine who can do what. Didn’t immediately realize thatmsg.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 likeacc1
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 themsg.sender
, whileacc0
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 usetry-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: