Truffle: Testing Smart Contracts
In our introduction to Truffle, we discussed what Truffle is and how it can help you automate the job of compiling, testing and deploying smart contracts.
In this article, we’ll explore how to test smart contracts. Testing is the most important aspect of quality smart contract development.
Why test extensively? So you can avoid stuff like this, or this. Smart contracts handle value, sometimes a huge amount of value — which makes them very interesting prey for people with the time and the skills to attack them.
You wouldn’t want your project to end up on the Blockchain graveyard, would you?
Getting Started
We’re going to be making HashMarket, a simple, smart-contract based used goods market.
Open your terminal and position yourself in the folder where you want to build the project. In that folder, run the following:
mkdir HashMarket
cd HashMarket
truffle init
You should get a result that looks a little bit like this:
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
You’ll also get a file structure that looks like this:
.
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js
For a refresher on the files, take a look at the previous article. In a nutshell, we have the basic truffle.js
and the two files used for making the initial migrations onto the blockchain.
Preparing the test environment
The easiest way to test is on the local network. I highly recommend using the ganache-cli
(previously known as TestRPC
) tool for contract testing.
Install ganache-cli
(which requires the Node Package Manager):
npm install -g ganache-cli
After that, open a separate terminal window or tab and run this:
ganache-cli
You should see an output similar to this:
Ganache CLI v6.1.0 (ganache-core: 2.1.0)
Available Accounts
==================
(0) 0xd14c83349da45a12b217988135fdbcbb026ac160
(1) 0xc1df9b406d5d26f86364ef7d449cc5a6a5f2e8b8
(2) 0x945c42c7445af7b3337834bdb1abfa31e291bc40
(3) 0x56156ea86cd46ec57df55d6e386d46d1bbc47e3e
(4) 0x0a5ded586d122958153a3b3b1d906ee9ff8b2783
(5) 0x39f43d6daf389643efdd2d4ff115e5255225022f
(6) 0xd793b706471e257cc62fe9c862e7a127839bbd2f
(7) 0xaa87d81fb5a087364fe3ebd33712a5522f6e5ac6
(8) 0x177d57b2ab5d3329fad4f538221c16cb3b8bf7a7
(9) 0x6a146794eaea4299551657c0045bbbe7f0a6db0c
Private Keys
==================
(0) 66a6a84ee080961beebd38816b723c0f790eff78f0a1f81b73f3a4c54c98467b
(1) fa134d4d14fdbac69bbf76d2cb27c0df1236d0677ec416dfbad1cc3cc058145e
(2) 047fef2c5c95d5cf29c4883b924c24419b12df01f3c6a0097f1180fa020e6bd2
(3) 6ca68e37ada9b1b88811015bcc884a992be8f6bc481f0f9c6c583ef0d4d8f1c9
(4) 84bb2d44d64478d1a8b9d339ad1e1b29b8dde757e01f8ee21b1dcbce50e2b746
(5) 517e8be95253157707f34d08c066766c5602e519e93bace177b6377c68cba34e
(6) d2f393f1fc833743eb93f108fcb6feecc384f16691250974f8d9186c68a994ef
(7) 8b8be7bec3aca543fb45edc42e7b5915aaddb4138310b0d19c56d836630e5321
(8) e73a1d7d659b185e56e5346b432f58c30d21ab68fe550e7544bfb88765235ae3
(9) 8bb5fb642c58b7301744ef908fae85e2d048eea0c7e0e5378594fc7d0030f100
HD Wallet
==================
Mnemonic: ecology sweet animal swear exclude quote leopard erupt guard core nice series
Base HD Path: m/44'/60'/0'/0/{account_index}
Listening on localhost:8545
This is a listing of all the accounts ganache-cli
created for you. You can use any account you wish, but these accounts will be preloaded with ether, so that makes them very useful (since testing requires ether for gas costs).
After this, go to your truffle.js
or truffle-config.js
file and add a development network to your config:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*"
}
}
};
Writing the smart contract
The first thing we’ll do is write the HashMarket smart contract. We’ll try to keep it as simple as possible, while still retaining the required functionality.
HashMarket is a type of eBay on the blockchain. It enables sellers to post products and buyers to buy them for ether. It also enables sellers to remove products if they’re not sold.
In your project, in the contracts
folder, create a new file and call it HashMarket.sol
. In that file, add the following code:
pragma solidity 0.4.21;
contract HashMarket {
// Track the state of the items, while preserving history
enum ItemStatus {
active,
sold,
removed
}
struct Item {
bytes32 name;
uint price;
address seller;
ItemStatus status;
}
event ItemAdded(bytes32 name, uint price, address seller);
event ItemPurchased(uint itemID, address buyer, address seller);
event ItemRemoved(uint itemID);
event FundsPulled(address owner, uint amount);
Item[] private _items;
mapping (address => uint) public _pendingWithdrawals;
modifier onlyIfItemExists(uint itemID) {
require(_items[itemID].seller != address(0));
_;
}
function addNewItem(bytes32 name, uint price) public returns (uint) {
_items.push(Item({
name: name,
price: price,
seller: msg.sender,
status: ItemStatus.active
}));
emit ItemAdded(name, price, msg.sender);
// Item is pushed to the end, so the lenth is used for
// the ID of the item
return _items.length - 1;
}
function getItem(uint itemID) public view onlyIfItemExists(itemID)
returns (bytes32, uint, address, uint) {
Item storage item = _items[itemID];
return (item.name, item.price, item.seller, uint(item.status));
}
function buyItem(uint itemID) public payable onlyIfItemExists(itemID) {
Item storage currentItem = _items[itemID];
require(currentItem.status == ItemStatus.active);
require(currentItem.price == msg.value);
_pendingWithdrawals[currentItem.seller] = msg.value;
currentItem.status = ItemStatus.sold;
emit ItemPurchased(itemID, msg.sender, currentItem.seller);
}
function removeItem(uint itemID) public onlyIfItemExists(itemID) {
Item storage currentItem = _items[itemID];
require(currentItem.seller == msg.sender);
require(currentItem.status == ItemStatus.active);
currentItem.status = ItemStatus.removed;
emit ItemRemoved(itemID);
}
function pullFunds() public returns (bool) {
require(_pendingWithdrawals[msg.sender] > 0);
uint outstandingFundsAmount = _pendingWithdrawals[msg.sender];
if (msg.sender.send(outstandingFundsAmount)) {
emit FundsPulled(msg.sender, outstandingFundsAmount);
return true;
} else {
return false;
}
}
}
After you’ve done this, try running truffle compile
to see if the code will compile. Since Solidity tends to change conventions, if your code won’t compile, the likely solution is using an older version of the compiler (0.4.21. is the version this was written with and will be fine).
Writing the migration
You need to write a migration to let Truffle know how to deploy your contract to the blockchain. Go into the migrations
folder and create a new file called 2_deploy_contracts.js
. In that file, add the following code:
var HashMarket = artifacts.require("./HashMarket.sol");
module.exports = function(deployer) {
deployer.deploy(HashMarket);
};
Since we only have one contract, the migrations file is very simple.
Now run truffle migrate
and hopefully you’ll get something like this:
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xad501b7c4e183459c4ee3fee58ea9309a01aa345f053d053b7a9d168e6efaeff
Migrations: 0x9d69f4390c8bb260eadb7992d5a3efc8d03c157e
Saving successful migration to network...
... 0x7deb2c3d9dacd6d7c3dc45dc5b1c6a534a2104bfd17a1e5a93ce9aade147b86e
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying HashMarket...
... 0xcbc967b5292f03af2130fc0f5aaced7080c4851867abd917d6f0d52f1072d91e
HashMarket: 0x7918eaef5e6a21a26dc95fc95ce9550e98e789d4
Saving successful migration to network...
... 0x5b6a332306f739b27ccbdfd10d11c60200b70a55ec775e7165358b711082cf55
Saving artifacts...
Testing Smart Contracts
You can use Solidity or JavaScript for smart contract testing. Solidity can be a little bit more intuitive when testing smart contracts, but JavaScript gives you many more possibilities.
Solidity testing
In order to start testing, in the test
folder in your project, create a file called TestHashMarket.sol
. The Truffle suite provides us with helper functions for testing, so we need to import those. At the beginning of the file, add:
pragma solidity ^0.4.20;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/HashMarket.sol";
The first two imports are the important ones.
The Assert
import gives us access to various testing functions, like Assert.equals()
, Assert.greaterThan()
, etc. In this manner, Assert works with Truffle to automate most “boring” code writing.
The DeployedAddresses
import manages contract addresses for us. Since every time you change your contract, you must redeploy it to a new address and even if you don’t change it, every time you test the contract should be redeployed to start from pristine state. The DeployedAddresses
library manages this for us.
Now let’s write a test. Under the import
directives, add the following:
contract TestHashMarket {
function testAddingNewProduct() public {
// DeployedAddresses.HashMarket() handles contract address
// management for us
HashMarket market = HashMarket(DeployedAddresses.HashMarket());
bytes32 expectedName = "T";
uint expectedPrice = 1000;
uint itemID = market.addNewItem(expectedName, expectedPrice);
bytes32 name;
uint price;
address seller;
uint status;
(name, price, seller, status) = market.getItem(itemID);
Assert.equal(name, expectedName, "Item name should match");
Assert.equal(price, expectedPrice, "Item price should match");
Assert.equal(status, uint(HashMarket.ItemStatus.active), "Item status at creation should be .active");
Assert.equal(seller, this, "The function caller should be the seller");
}
}
Let’s look at some of the important parts of a test. Firstly:
HashMarket market = HashMarket(DeployedAddresses.HashMarket());
This code uses the DeployedAddresses
library to create a new instance of
the HashMarket
contract for testing.
Assert.equal(<current>, <expected>, <message>)
This part of the code takes care of checking whether two values are equal. If yes, it communicates a success message to the test suite. If not, it communicates a failure. It also appends the message so you can know where your code failed.
Now let’s run this:
truffle test
You should get a result like this:
TestHashMarket
1) testAddingNewProduct
> No events were emitted
0 passing (879ms)
1 failing
1) TestHashMarket testAddingNewProduct:
Error: VM Exception while processing transaction: revert
at Object.InvalidResponse (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1)
at /usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/requestmanager.js:86:1
at /usr/local/lib/node_modules/truffle/build/webpack:/~/truffle-provider/wrapper.js:134:1
at XMLHttpRequest.request.onreadystatechange (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/httpprovider.js:128:1)
at XMLHttpRequestEventTarget.dispatchEvent (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:64:1)
at XMLHttpRequest._setReadyState (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:354:1)
at XMLHttpRequest._onHttpResponseEnd (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:509:1)
at IncomingMessage.<anonymous> (/usr/local/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:469:1)
at endReadableNT (_stream_readable.js:1106:12)
at process._tickCallback (internal/process/next_tick.js:178:19)
Our test failed :-(
Let’s go into the contract to inspect possible errors.
After a careful inspection, we’ll find that the issue with our contract is in the return
statement of our addNewItem
method:
function addNewItem(bytes32 name, uint price) public returns (uint) {
_items.push(Item({
name: name,
price: price,
seller: msg.sender,
status: ItemStatus.active
}));
// Item is pushed to the end, so the lenth is used for
// the ID of the item
return _items.length;
}
Since arrays are zero indexed, and we use the array position as an ID, we should actually return _items.length - 1
. Fix this error and run this again:
truffle test
You should get a much happier message:
TestHashMarket
✓ testAddingNewProduct (130ms)
1 passing (729ms)
We’ve successfully used Truffle testing to fix a very likely error in our code!
JavaScript Testing
Truffle enables us to use JavaScript for testing, leveraging the Mocha testing framework. This enables you to write more complex tests and get more functionality out of your testing framework.
Okay, let’s write the test. First, in the test
folder, create a file and call it hashmarket.js
.
The first thing we need to do, is get the reference to our contract in JavaScript. For that, we’ll use Truffle’s artifacts.require(...)
function:
var HashMarket = artifacts.require("./HashMarket.sol");
Now that we have the reference to the contract, let’s start writing tests. To start, we’ll use the contract
function provided to us:
contract("HashMarket", function(accounts) {
});
This creates a test suite for our contract. Now for testing, we use Mocha it
syntax:
contract("HashMarket", function(accounts) {
it("should add a new product", function() {
});
});
This describes the test we’ll write and presents a message for us to know the purpose of the test. Now let’s write the test itself. At the end, the hashmarket.js
file should look like the following. The reasoning is explained in the comments of the source code:
var HashMarket = artifacts.require("./HashMarket.sol");
contract("HashMarket", function(accounts) {
it("should add a new product", function() {
// Set the names of test data
var itemName = "TestItem";
var itemPrice = 1000;
var itemSeller = accounts[0];
// Since all of our testing functions are async, we store the
// contract instance at a higher level to enable access from
// all functions
var hashMarketContract;
// Item ID will be provided asynchronously so we extract it
var itemID;
return HashMarket.deployed().then(function(instance) {
// set contract instance into a variable
hashMarketContract = instance;
// Subscribe to a Solidity event
instance.ItemAdded({}).watch((error, result) => {
if (error) {
console.log(error);
}
// Once the event is triggered, store the result in the
// external variable
itemID = result.args.itemID;
});
// Call the addNewItem function and return the promise
return instance.addNewItem(itemName, itemPrice, {from: itemSeller});
}).then(function() {
// This function is triggered after the addNewItem call transaction
// has been mined. Now call the getItem function with the itemID
// we received from the event
return hashMarketContract.getItem.call(itemID);
}).then(function(result) {
// The result of getItem is a tuple, we can deconstruct it
// to variables like this
var [name, price, seller, status] = result;
// Start testing. Use web3.toAscii() to convert the result of
// the smart contract from Solidity bytecode to ASCII. After that
// use the .replace() to pad the excess bytes from bytes32
assert.equal(itemName, web3.toAscii(name).replace(/\u0000/g, ''), "Name wasn't properly added");
// Use assert.equal() to check all the variables
assert.equal(itemPrice, price, "Price wasn't properly added");
assert.equal(itemSeller, seller, "Seller wasn't properly added");
assert.equal(status, 0, "Status wasn't properly added");
});
});
});
Run truffle test
and you should get something like this:
TestHashMarket
✓ testAddingNewProduct (109ms)
Contract: HashMarket
✓ should add a new product (64ms)
2 passing (876ms)
Your test has passed and you can be confident about not having regression bugs.
For homework, write tests for all other functions in the contract.