Smart Contract Unit Testing — Use ERC20 Token Contract as an Example
Before we start
Like everyone, I am using Truffle Framework and Testrpc to speed up smart contract development. This article only focus on Ethereum smart contract testing. Therefore, I will skip the environment set-up and tool installation here and just summarize some key points before start testing.
- I’m using Ubuntu 16.04 LTS.
- Install Truffle Framework globally. In this article, I will simply “init” a project.
- I choose Testrpc as my Ethereum client.
- I use the code of “Sample Fixed Supply Token Contract” on this link. Just run “truffle create contract myContract” in your project and copy&paste. Since “throw” in the sample is deprecated, change it into “revert()”.
- I will write tests in JavaScript. Because Truffle uses the Mocha testing framework and Chai as assertion library, I recommend learning their basic use. Also, knowledge of JavaScript Promise object is needed.
- Understanding six functions and two events of ERC20 token standard.
Some preparations
Create a raw contract and name it FixedSupplyToken.sol. After finish the code, make sure there is no compilation error in the ERC20 token contract.
$ truffle compile
Start Testrpc.
$ testrpc
2_deploy_contracts.js in your project should look like this or something like this.
var Token = artifacts.require("FixedSupplyToken");module.exports = function(deployer) {
deployer.deploy(Token);
};
Deploy the ERC20 token contract to Testrpc.
$ truffle migrate
Create a test .js file for this contract.
$ truffle create test fixedsupplytoken
Then you will have fixedsupplytoken.js in test directory in your project. Edit fixedsupplytoken.js.
var Token = artifacts.require("FixedSupplyToken");contract('fixedsupplytoken', function(accounts) {
it("should assert true", function() {
var token;
return Token.deployed().then(function(instance){
token = instance;
return token.totalSupply.call();
}).then(function(result){
assert.equal(result.toNumber(), 1000000, 'total supply is wrong');
})
});
});
artifacts.require() method returns contract abstraction which we can use within the rest of our test script. In the code above, I simply called totalSupply function(no transaction sent) of the contract. Because the function returns promise, I attach my callbacks by using then. For simplicity, failure callback is omitted. When totalSupply finishes(asynchronous request finished), success callback is called. Parameter of success callback, result, is now return value of totalSupply function. In the end, I use assert.equal to check equality.
Run the test.
$ truffle test ./test/fixedsupplytoken.js
We conquered a simple unit test. After that, we will use similar tricks to fully go through every function of ERC20 token contract. Let’s continue with fixedsupplytoken.js.
Test every function
First, I would like to test balanceOf function. Add the following code in fixedsupplytoken.js(after it(…)).
it("should return the balance of token owner", function() {
var token;
return Token.deployed().then(function(instance){
token = instance;
return token.balanceOf.call(accounts[0]);
}).then(function(result){
assert.equal(result.toNumber(), 1000000, 'balance is wrong');
})
});
Almost same code here. Difference is that I pass accounts[0] to call function, meaning that I would like to know the balance of accounts[0]. Accounts[0] is the first address offered by testrpc. We can access the other 9 addresses with accounts[1], accounts[2]….Since default address for deploying contract is accounts[0], and when contract is deployed, the one who send transaction will get all token(see constructor). The return value of this function call will be 1,000,000 again. Run the test, you will see 2 passing now.
Test transfer function.
it("should transfer right token", function() {
var token;
return Token.deployed().then(function(instance){
token = instance;
return token.transfer(accounts[1], 500000);
}).then(function(){
return token.balanceOf.call(accounts[0]);
}).then(function(result){
assert.equal(result.toNumber(), 500000, 'accounts[0] balance is wrong');
return token.balanceOf.call(accounts[1]);
}).then(function(result){
assert.equal(result.toNumber(), 500000, 'accounts[1] balance is wrong');
})
});
You will see that I call transfer function without .call. This is because I want to send transaction and make permanent changes to blockchain here. .call simply read and return data, make no changes on blockchain. Right now, if transfer function works well, both accounts[0] and accounts[1] will have 500,000. Run the test again.
Now, I will test transferFrom, approve and allowance function altogether, because they need each other to form a complete purpose — give someone authority to send your own token at a specific amount.
I assume that changes of the unit tests above remain, so accounts[0] and accounts[1] have 500,000 now.
it("should give accounts[1] authority to spend account[0]'s token", function() {
var token;
return Token.deployed().then(function(instance){
token = instance;
return token.approve(accounts[1], 200000);
}).then(function(){
return token.allowance.call(accounts[0], accounts[1]);
}).then(function(result){
assert.equal(result.toNumber(), 200000, 'allowance is wrong');
return token.transferFrom(accounts[0], accounts[2], 200000, {from: accounts[1]});
}).then(function(){
return token.balanceOf.call(accounts[0]);
}).then(function(result){
assert.equal(result.toNumber(), 300000, 'accounts[0] balance is wrong');
return token.balanceOf.call(accounts[1]);
}).then(function(result){
assert.equal(result.toNumber(), 500000, 'accounts[1] balance is wrong');
return token.balanceOf.call(accounts[2]);
}).then(function(result){
assert.equal(result.toNumber(), 200000, 'accounts[2] balance is wrong');
})
});
In this test, accounts[0] authorize(approve function) accounts[1] to transfer 200,000 using accounts[0] balance. Then, calling allowance function will return the amount of token being authorized to spend. It should be 200,000. Right now, accounts[1] can use accounts[0]’s token with the limitation of sending 200,000 at most.
After calling approve, we can test transferFrom. To test this, we should send transaction with accounts[1], because it is accounts[1] having capacity to send. In this example, I transfer 200,000 from accounts[0] to accounts[2]. Therefore, you will see token.transferFrom(accounts[0], accounts[2], 200000, {from: accounts[1]}). The last parameter is transaction object which can specify sending address. In the rest of code, I am checking all the address balances. Run the test, another passing should show up.
In the end, command line should look like this.
How do I test an event? Actually, I don’t know if this is a good way. Here is how I do. You will see “Transfer”
it("should show the transfer event", function() {
var token;
return Token.deployed().then(function(instance){
token = instance;
return token.transfer(accounts[1], 100000);
}).then(function(result){
console.log(result.logs[0].event)
})
});
Summary
I have run all the code above by myself and I am still learning. If there are some errors, please let me know. Hope this humble article, also a notes for myself, can help you understand basic smart contract testing with Truffle.