How to Test Smart Contracts – Tutorial
Testing a smart contract is a must of the blockchain development process.
Remember that the blockchain does not forgive you any errors because of its immutability!
The only way to fix a bug on a deployed smart contract is to deploy a new version of that contract; the old version with the bug will be still on the blockchain and it will remain there forever.
So testing the smart contract before deploying it will make sure your functions will have the expected behavior.
Luckily, the truffle framework makes the testing process very easy. In this tutorial we are going to use Mocha to test our smart contracts.
We are going to test the smart contract created on the tutorial Create your Blockchain DApp with Ethereum and VueJS.
Below you can find the link to download the full project and the link to the test file we are going to create in this tutorial:
If you are familiar with Javascript and ethereum smart contract I think it will be easier to see straight away the test file clicking the button before. The code is well commented.
Set up the project to test
Let’s clone from the git repository the DApp developed in the previous lesson. Open the terminal and run the command:
git clone https://github.com/danielefavi/ethereum-vuejs-dapp.git
and then let’s cd in the root project folder:
cd ethereum-vuejs-dapp
Then please delete the test
folder since we are going to create the test from scratch.
What do we want to test?
Before developing the test we must define what to test.
To test your smart contracts you must have a list of the functionalities and restrictions you want to be tested. In our case we are going to test:
- Register a user with a username and a status.
- A registered user can update his own profile.
- A user cannot be registered twice.
Set up the test
What we want to do is to test the smart contract ./contracts/User.sol
. So first let’s create the folder test
inside root of the truffle project:
mkdir test
Then run in the terminal the following command to create the testing file (it will create the file users.js
where we are going to write our testing code):
truffle create test Users
NOTE: if you run the command truffle create test Users
without creating the folder test
you can get the error:
Error: ENOENT: no such file or directory, open ‘~\ethereum-vuejs-dapp\test\users.js’
Now open the file test\users.js
and remove all the default content in it.
In the beginning of the file users.js
we have to import in the smart contract we want to test: User.sol
. To do so, we can use the code:
var Users = artifacts.require(‘./Users.sol’);
The artifacts.require()
method is similar to Node’s require
. In this case it returns a contract abstraction that we can use within the rest of our deployment script. The name specified should match the name of the contract definition within that source file (more info).
Then we have to use the contract()
function to actually test the functions in the smart contract:
var Users = artifacts.require('./Users.sol');
contract('Users', function(accounts) {
var mainAccount = accounts[0];
// CODE OF THE TEST CASES HERE
});
The first parameter of contract()
is the name of the smart contract and the second parameter is the callback function where you will write your test code.
Note that a list of accounts is passed to the callback function.
DEVELOPING the Test case 1
Register a user with a username and status
This test case will be executed following the steps:
- Register a new user using the main account:
accounts[0]
- After the registration we are going to check the total number of registered users is increased by one.
- Check that the wallet address used for the registration is correctly registered.
- Check the username and status used for the registration matches with the username and status stored in the blockchain.
Let’s define a test case called should register a user using Mocha; then we are going to get the smart contract instance in order to be able to call the contract functions.
contract('Users', function(accounts) {
var mainAccount = accounts[0];
it("should register an user", function() {
var usersBeforeRegister = null;
return Users.deployed().then(function(contractInstance) {
// storing the contract instance so it will be used later on
instance = contractInstance;
…
});
}); // end of "should register an user"
});
Once we got the contract instance we have to make a series of calls to the smart contract (using Promises) to:
- Get the current number of registered users and store it in the variable
usersBeforeRegister
; here we are going to call the contract functiontotalUsers
- Register the user using the smart contract function
registerUser
. - Get again the number of the registered user and check that the new total is increased by one compared to
usersBeforeRegister
. - Check if the wallet address used for the registration is registered.
To keep this test case simple I moved the step 4 to a dedicated test case.
This is the full code of the first test case:
it("should register an user", function() {
var usersBeforeRegister = null;
return Users.deployed().then(function(contractInstance) {
// storing the contract instance so it will be used later on
instance = contractInstance;
// calling the smart contract function totalUsers to get the current number of users
return instance.totalUsers.call();
}).then(function(result) {
// storing the current number on the var usersBeforeRegister
usersBeforeRegister = result.toNumber();
// registering the user calling the smart contract function registerUser
return instance.registerUser('Test User Name', 'Test Status', {
from: mainAccount
});
}).then(function(result) {
return instance.totalUsers.call();
}).then(function(result) {
// checking if the total number of user is increased by 1
assert.equal(result.toNumber(), (usersBeforeRegister+1), "number of users must be (" + usersBeforeRegister + " + 1)");
// calling the smart contract function isRegistered to know if the sender is registered.
return instance.isRegistered.call();
}).then(function(result) {
// we are expecting a boolean in return that it should be TRUE
assert.isTrue(result);
});
}); // end of "should register an user"
The test is passed when all the assertions within the test case are satisfied. In this case we are asserting that the number of subscribers is increased by 1 after the registration:
assert.equal(result.toNumber(), (usersBeforeRegister+1), “number of users must be (" + usersBeforeRegister + " + 1)");
and the user account is registered:
…
return instance.isRegistered.call();
}).then(function(result) {
assert.isTrue(result);
});
To check if the username and status stored in the blockchian are correct (step 4) we can define the following test case:
it("username and status in the blockchian should be the same the one gave on the registration", function() {
// NOTE: the contract instance has been instantiated before, so no need
// to do again: return Users.deployed().then(function(contractInstance) { …
// like before in "should register an user".
return instance.getOwnProfile.call().then(function(result) {
// the result is an array where in the position 0 there user ID, in
// the position 1 the user name and in the position 2 the status,
assert.equal(result[1], 'Test User Name');
// the status is type of bytes32: converting the status Bytes32 into string
let newStatusStr = web3.toAscii(result[2]).replace(/\u0000/g, '');
assert.equal(newStatusStr, 'Test Status');
});
}); // end testing username and status
This test case is passed when the username and status on the blockchain are equal to the username and status that we gave on the registration phase:
assert.equal(result[1], 'Test User Name');
let newStatusStr = web3.toAscii(result[2]).replace(/\u0000/g, '');
assert.equal(newStatusStr, 'Test Status');
Note that the status is stored in Bytes32 so we have to convert this value to string.
Another important note: a web3 instance is available in each test file, configured to the correct provider!
DEVELOPING the Test case 2
A registered user can update his own profile
In this test case we have to check if the smart contract function that updates the user’s profile works correctly:
- Call the smart contract function
updateUser
passing the new username and the new status as parameters. - Call the smart contract function
getOwnProfile
to get the user profile. - Check if the username and status from the result of
getOwnProfile
are equal to the given username and status of the first step.
it("should update the profile", function() {
return instance.updateUser('Updated Name', 'Updated Status', {
from: mainAccount
}).then(function(result) {
return instance.getOwnProfile.call();
}).then(function(result) {
// the result is an array where in the position 0 there user ID, in
// the position 1 the user name and in the position 2 the status,
assert.equal(result[1], 'Updated Name');
// the status is type of bytes32: converting the status Bytes32 into string
let newStatusStr = web3.toAscii(result[2]).replace(/\u0000/g, '');
assert.equal(newStatusStr, 'Updated Status');
});
}); // end should update the profile
DEVELOPING the Test case 3
A user cannot be registered twice
In this test case we will try to register a user that has already been registered.
Please first check the function addUser
in the smart contract User.sol
. The function addUser
first checks if the wallet address is already registered; if the user is already registered then the registration process will be stopped:
// checking if the user is already registered
uint userId = usersIds[_wAddr];
require (userId == 0);
So if we are trying to register a user twice we are expecting the registration to fail. So a failed registration means that the test is passed:
it("a registered user should not be registered twice", function() {
// we are expecting the call to registerUser to fail since the user account
// is already registered!
return instance.registerUser('Test username Twice', 'Test Status Twice', {
from: mainAccount
}).then(assert.fail).catch(function(error) { // here we are expecting the exception
assert(true);
});
}); // end testing registration twice
LET’S RUN THE TEST
Finally let’s run the test on the console with the following command:
truffle test
this command will run all the tests in the folder test
. If you want to run only a specific test file you can use the command:
truffle test .\test\users.js
If all the tests are passed, on the terminal you should see something like this:_
Sources
Truffle framework DOCS: https://truffleframework.com/docs/truffle/testing/testing-your-contracts