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.
Frequently Asked Questions (FAQs) about Truffle Testing Smart Contracts
What are the prerequisites for testing smart contracts with Truffle?
Before you start testing smart contracts with Truffle, you need to have a basic understanding of Ethereum, Solidity, and how smart contracts work. You should also have Node.js and npm installed on your computer. Truffle is a development environment, testing framework, and asset pipeline for Ethereum, and you can install it using npm. You also need to have Ganache, a personal blockchain for Ethereum development that you can use to deploy contracts, develop applications, and run tests.
How do I write tests in Solidity?
Writing tests in Solidity involves creating a new contract that inherits from the contract you want to test. You can then call the functions of the contract and use Solidity’s built-in assert function to check the results. For example, if you have a contract called MyContract, you could create a test contract like this:contract TestMyContract {
MyContract myContract;
function beforeEach() public {
myContract = new MyContract();
}
function testInitialBalance() public {
uint expected = 10000;
assert(myContract.balance() == expected);
}
}
How do I debug tests in Truffle?
Truffle provides a built-in debugger that you can use to step through your code and inspect variables. To use the debugger, you first need to run your tests with the –debug flag. This will output a list of transaction hashes for each test. You can then use the truffle debug command followed by a transaction hash to start a debugging session.
How do I write tests in JavaScript?
Truffle supports writing tests in JavaScript using the Mocha testing framework and the Chai assertion library. A JavaScript test file in Truffle looks similar to a regular Mocha test file, but with some additional functions for interacting with your contracts. Here’s an example:const MyContract = artifacts.require("MyContract");
contract("MyContract", accounts => {
it("should have an initial balance of 10000", async () => {
const myContract = await MyContract.deployed();
const balance = await myContract.balance();
assert.equal(balance.valueOf(), 10000, "initial balance is not 10000");
});
});
How do I handle errors in Truffle tests?
When a function call in a Truffle test fails, it throws an exception. You can catch this exception using a try/catch block and then use the error message to assert that the failure was expected. For example:it("should throw an error when trying to transfer more than balance", async () => {
const myContract = await MyContract.deployed();
try {
await myContract.transfer(accounts[1], 10001);
} catch (error) {
assert(error.message.includes("revert"), "expected revert error");
}
});
How do I test contract interactions in Truffle?
You can test interactions between multiple contracts by deploying them in your tests and then calling their functions. Truffle provides the artifacts.require function for importing contract artifacts, and the deployed and new functions for getting instances of your contracts.
How do I use the Truffle console?
The Truffle console is a REPL that allows you to interact with your contracts and the Ethereum network. You can start the console with the truffle console command. Once in the console, you can execute any command or script in the context of your Truffle environment.
How do I test contract events in Truffle?
Contract events can be tested in Truffle by calling a contract function that emits an event and then checking the returned transaction receipt for the event. The receipt contains an array of all the events that were emitted during the transaction.
How do I use Ganache for testing?
Ganache is a personal blockchain for Ethereum development that you can use to deploy contracts, develop applications, and run tests. You can start Ganache with the ganache-cli command. Once Ganache is running, you can configure Truffle to connect to it by setting the host and port in your truffle-config.js file.
How do I test contract upgrades in Truffle?
Testing contract upgrades involves deploying a new version of a contract and then checking that the state and functionality of the contract have been preserved. You can use the OpenZeppelin Upgrades plugin for Truffle to automate this process.
Mislav Javor is a software engineer and CEO (in that order). He writes smart contracts, conducts lectures, and blogs at mislavjavor.com.