Test A Smart Contract With Truffle

arjuna sky kok
Coinmonks
Published in
10 min readMay 4, 2018

--

In the previous article, I showed you how to write the smart contract in the Remix editor. But it’s not convenient to write the smart contract on the web directly. For learning purpose, writing the code on the web directly is fine. For blogging, writing the article on the web directly is fine. But when you do coding for serious purpose, it’s usually offline. So I’ll show you how to write a smart contract with a normal text editor.

Truffle is like a framework to build the smart contract. You don’t have to use Truffle to write the smart contract. But Truffle is to coding a smart contract is like Ruby on Rails to web programming.

First, you need NodeJs. Go to its homepage and install the recommended version (at the time of this article is written, the version is 8).

NodeJs homepage

Installing NodeJs gives you node and npm programs. Node is something like java, python, ruby programs. It runs the JavaScript application. Npm is the package manager. It is similar to pip, apt-get, gem.

Then you need to install Truffle:

npm install -g truffle

If you are in MacOSX or Ubuntu Linux, you may need to append sudo in front of the command to get the permission to install truffle command system wide.

Truffle homepage

We create a directory and initialize it with Truffle.

mkdir Auctioncd Auctiontruffle init

Inside the directory, you put the smart contract inside the contracts directory.

edit contracts/Auction.sol

You can replace edit with the command to launch your beloved text editor, such as vim, code, sublime, emacs, or atom.

Copy this code to Auction.sol.

pragma solidity ^0.4.19;contract Auction {
address public manager;
address public seller;
uint public latestBid;
address public latestBidder;

constructor() public {
manager = msg.sender;
}

function auction(uint bid) public {
latestBid = bid * 1 ether; //1000000000000000000;
seller = msg.sender;
}

function bid() public payable {
require(msg.value > latestBid);

if (latestBidder != 0x0) {
latestBidder.transfer(latestBid);
}
latestBidder = msg.sender;
latestBid = msg.value;
}

function finishAuction() restricted public {
seller.transfer(address(this).balance);
}

modifier restricted() {
require(msg.sender == manager);
_;
}
}

If you want to know what this code does, check my previous article.

You can compile the smart contract by launching this command.

truffle compile

By default the output of your compilation process is in build/contracts/Auction.json file. You can open it to see what it is inside. It is a json file. There are two keys of this json object that are very important, which are abi and bytecode.

The bytecode is the content that the Ethereum Virtual Machine understands. It’s like the binary file. The abi is the interface so we can interact with the smart contract. So if the bytecode is the house, the abi is the map of the house (where the doors are located).

You can not execute this bytecode just like that. It’s different than when you compile the C/C++ code and be able to execute the binary directly. You have to put the smart contract in the blockchain first.

For simplicity, you are going to deploy this smart contract to the local blockchain. The way to launch the smart contract to Ethereum main network where real Ethereum lives is similar. But first you launch the local blockchain with Truffle.

truffle develop
Truffle develop

Did you see by default it launches the service in port 9545 in localhost? Keep that in mind.

Before you can deploy your beloved smart contract, you need to tell the Truffle project where to look for the blockchain. Edit the truffle config file.

edit truffle.js

Copy this content into that file.

module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
networks: {
“development”: {
network_id: 2,
host: “localhost”,
port: 9545
},
}
};

Then we need to write the migration. To some people who are familiar with frameworks like Ruby on Rails, migration in this case is not like database migration. Here, migration means the process to deploy that particular smart contract.

edit migrations/2_deploy_contracts.js

Copy this content to that file.

var Auction = artifacts.require(“Auction”);module.exports = function(deployer) {
deployer.deploy(Auction);
};

Now you can deploy the smart contract. Open a new terminal and go inside the project directory. Don’t disrupt the terminal where you launched truffle develop. We will use it later.

truffle migrate

You will get the output like this:

Using network ‘development’.Running migration: 1_initial_migration.js
Replacing Migrations…
… 0x3a8f8c4379f1d7269b9d931a088984f4ccfeec797bbb6e128582e2915c8f3ea1
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network…
… 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts…
Running migration: 2_deploy_contracts.js
Replacing Auction…
… 0xf0b10908aeb415b8b13fe60aeed8ade6769679572a44cf679b5616e38670329d
Auction: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving artifacts…

Your output will not be same 100% with mine. The addresses will be different. Take at look at the deployed address. It is important. In my case, the address of deployed Auction smart contract is 0x345ca3e014aaf5dca488057592ee47305d9b3e10.

Now, go to Truffle console. It is in the terminal where you launched truffle develop. And take at look at the first public key. Take notice of that.

Look at the first account

Let’s find out the manager of this smart contract. Type this in Truffle develop console.

Auction.at(“0x345ca3e014aaf5dca488057592ee47305d9b3e10”).manager.call();

The random number, 0x345ca3e014aaf5dca488057592ee47305d9b3e10 is from the output of truffle migrate command. The output of this statement you typed just now in Truffle develop console is the first account address that I asked you take notice of. It looks like truffle migrate command will deploy the smart contract with the first account in Truffle develop.

Now, let’s execute the auction method but with different account.

Auction.at(“0x345ca3e014aaf5dca488057592ee47305d9b3e10”).auction(3, {from: “0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e” });

You can check the result whether this method works properly or not.

truffle(develop)> Auction.at(“0x345ca3e014aaf5dca488057592ee47305d9b3e10”).seller.call();
‘0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e’

By the way, you can make it simpler by just doing this.

truffle(develop)> Auction.at(“0x345ca3e014aaf5dca488057592ee47305d9b3e10”).seller();
‘0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e’

Now, let’s write unit test for our smart contract. Here’s the bad news. I couldn’t write test successfully inside the Truffle framework. I have to do it manually. It’s either I did it wrong or there is a bug in Truffle framework. Once I figure it out, I’ll write it in future articles. For now, we just have to write the test manually. First install some stuff with npm.

npm install --save ganache-cli mocha web3@1.0.0-beta.34

This is local installation. It will save these libraries into this project, but not system wide. Ganache is like Truffle develop, a local blockchain for smart contract unit testing. Mocha is unit testing framework. The web3 is the way we connect to the deployed smart contract. Here, the web3 is not version 1.0 production ready yet. The production version is less than version 1.0 but we need version 1.0 because there are some methods we need badly. Hence, we use beta version.

Put the test file inside the test directory and name it Auction.test.js.

Write these lines.

const assert = require(‘assert’);const ganache = require(‘ganache-cli’);const Web3 = require(‘web3’);const web3 = new Web3(ganache.provider());const json = require(‘./../build/contracts/Auction.json’);

Here, we are importing important stuff from libraries we just installed. assert is to check the whether the method is working properly or not. ganache is our local blockchain. web3 is the way we connect to this local blockchain. json is a the output of the compilation of Auction.sol file.

Below that, write these lines.

let accounts;let auction;let manager;const interface = json[‘abi’];const bytecode = json[‘bytecode’];

We initialize some variables. As I described above, we only care about two parts of the output json file, the bytecode and the interface (abi).

Go on with these lines.

beforeEach(async () => {  accounts = await web3.eth.getAccounts();  manager = accounts[0];  auction = await new web3.eth.Contract(interface)      .deploy({ data: bytecode })      .send({ from: manager, gas: ‘1000000’ });});

beforeEach in unit testing means you run this method every time you execute a unit test. Connecting to blockchain is a asynchronous process so we have to declare our function async. The first thing we do is to get all accounts. Ganache gives us some accounts to play for. But we need these accounts before we go to other steps so we use await here to make the statement synchronous. We use the first account as manager. Then we deploy the contract. To deploy the contract, we need the interface, the bytecode, the address and some gas (deploying contract is not free).

describe(‘Auction’, () => {  it(‘deploys a contract’, async () => {    const auctionManager = await auction.methods.manager().call();    assert.equal(manager, auctionManager, “The manager is the one who   launches the smart contract.”);  });  //Continue from this line from now on...});

This is our first test. describe is a way of grouping test. The first test is to check the address which deploys the contract is saved to manager variable inside the smart contract. Here, we are interacting with the smart contract with NodeJs not Solidity. Hence, there are some differences of accessing the methods or properties of the smart contract. To access the manager property of the smart contract, you use this statement.

auction.methods.manager().call()

We continue our other test.

it(‘auctions the item’, async () => {  seller = accounts[1];  await auction.methods.auction(2).send({ from: seller });  auctionSeller = await auction.methods.seller().call();  assert.equal(auctionSeller, seller,“The seller is the one who called the auction method.”);  auctionBid = await auction.methods.latestBid().call();  assert.equal(auctionBid, web3.utils.toWei(‘2’, ‘ether’),“The latest bid is the argument sent to auction method converted into wei.”);});

Here, in this test, we use different account for seller. To execute the method of the smart contract with a specific address, use this statement:

auction.methods.auction(2).send({ from: seller });

We continue our journey with other test.

it(‘bids the item’, async () => {  bidder = accounts[2];  await auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘3’, ‘ether’) });  auctionBid = await auction.methods.latestBid().call();  assert.equal(auctionBid, web3.utils.toWei(‘3’, ‘ether’),“The latest bid is the payment sent to bid method converted into wei.”);});

Here, in this test, we test the sending payment when we execute the method of the smart contract. We use value keyword argument. By default, it accepts wei number. But we are working with ether number so we have to convert it with web3 helper function.

auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘3’, ‘ether’) });

We go on with other test.

it(‘must bid above the latest bid amount’, async () => {  bidder = accounts[2];  try {    await auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘1’, ‘ether’) });    assert(false);  } catch (err) {    assert(err);  }});

Here, in this test, we test the exception thrown when we fail the require statement in the smart contract. Remember this method in the smart contract?

function bid() public payable {
require(msg.value > latestBid);

if (latestBidder != 0x0) {
latestBidder.transfer(latestBid);
}
latestBidder = msg.sender;
latestBid = msg.value;
}

We have to send the payment greater than the last bid. So in this test we want to test the fail case. We use try catch to test the fail case.

try {  await auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘1’, ‘ether’) });  assert(false);} catch (err) {  assert(err);}

We continue with the last tests.

it(‘only manager can finish the auction’, async () => {  nonmanager = accounts[1];  try {    await auction.methods.finishAuction().send({ from: nonmanager });    assert(false);  } catch (err) {    assert(err);  }});it(‘finishes the auction as manager’, async () => {  manager = accounts[0];  await auction.methods.finishAuction().send({ from: manager });  assert(true);});

The complete test file code is here.

const assert = require(‘assert’);const ganache = require(‘ganache-cli’);const Web3 = require(‘web3’);const web3 = new Web3(ganache.provider());const json = require(‘./../build/contracts/Auction.json’);let accounts;let auction;let manager;const interface = json[‘abi’];const bytecode = json[‘bytecode’];beforeEach(async () => {  accounts = await web3.eth.getAccounts();  manager = accounts[0];  auction = await new web3.eth.Contract(interface)      .deploy({ data: bytecode })      .send({ from: manager, gas: ‘1000000’ });});describe(‘Auction’, () => {  it(‘deploys a contract’, async () => {    const auctionManager = await auction.methods.manager().call();    assert.equal(manager, auctionManager, “The manager is the one who launched the smart contract.”);  });  it(‘auctions the item’, async () => {    seller = accounts[1];    await auction.methods.auction(2).send({ from: seller });    auctionSeller = await auction.methods.seller().call();    assert.equal(auctionSeller, seller, “The seller is the one who called the auction method.”);    auctionBid = await auction.methods.latestBid().call();    assert.equal(auctionBid, web3.utils.toWei(‘2’, ‘ether’), “The latest bid is the argument sent to auction method converted into wei.”);  });  it(‘bids the item’, async () => {    bidder = accounts[2];    await auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘3’, ‘ether’) });    auctionBid = await auction.methods.latestBid().call();    assert.equal(auctionBid, web3.utils.toWei(‘3’, ‘ether’), “The latest bid is the payment sent to bid method converted into wei.”);  });  it(‘must bid above the latest bid amount’, async () => {    bidder = accounts[2];    try {      await auction.methods.bid().send({ from: bidder, value: web3.utils.toWei(‘1’, ‘ether’) });      assert(false);    } catch (err) {      assert(err);    }  });  it(‘only manager can finish the auction’, async () => {    nonmanager = accounts[1];    try {      await auction.methods.finishAuction().send({ from: nonmanager    });      assert(false);    } catch (err) {      assert(err);    }  });  it(‘finishes the auction as manager’, async () => {    manager = accounts[0];    await auction.methods.finishAuction().send({ from: manager });    assert(true);  });});

This is how you execute the test.

npm run test

You’ll get the output like this.

Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News

Also, Read

--

--

arjuna sky kok
Coinmonks

I do 3 things for my livelihood: I build mobile apps and web apps for clients, I write about blockchain and cryptocurrency, and I give tech corporate training.