In the previous chapter, you started to integrate unit testing into your development environment with Mocha. But you might have noticed that the unit test script didn’t look ideal. It has a fairly complex setup, which includes explicit instructions on compiling SimpleCoin and creating a contract factory. Each test also includes an explicit deployment statement that references the contract factory. The main drawback is that all this infrastructural code distracts from the main objective of unit tests, which is to focus on functional aspects of a contract. Another disadvantage is that if you wanted to create a new test suite to cover a different contract, you’d have to duplicate all this infrastructural code.
Wouldn’t it be nice if you had a way to simplify the deployment of a contract? This is the main objective of Truffle, an Ethereum contract development framework that focuses on streamlining deployment and consequently simplifying unit testing.
In this chapter, you’ll set up Truffle, and then you’ll use it to improve the compile -> deploy -> test cycle. The main focus will be on learning how to use the tool, so, to avoid getting distracted by contract-specific issues, you’ll reuse our good old SimpleCoin contract yet again. If you’re eager to implement something new, please bear with me: in the next chapter you’ll develop a brand new Dapp from scratch using Truffle!
You can install Truffle easily with Node.js npm, and then you can start to create a project. Install version 4.1.15, which is the one I have used, as follows:
C:>npm install -g [email protected]
In order to run my code smoothly, don’t install Truffle 5.0.0. My code is written against version 4.1.15, and is unlikely to work correctly under version 5.0.0.
You’ll start with a minimal project, and then you’ll integrate SimpleCoin in Truffle by walking through the entire development lifecycle, including the following steps:
Create a working directory for the SimpleCoin Truffle project. I’ve created mine as follows:
C:EthereumTruffleSimpleCoin
Open an OS shell and move to this directory:
C:>cd EthereumTruffleSimpleCoin
Now you can initialize the Truffle project:
C:EthereumTruffleSimpleCoin>truffle init
Truffle will create the following directory structure and prepopulate it with some files:
/contracts Migrations.sol /migrations 1_initial_migration.js /test truffle.js truffle-config.js
Table 11.1 provides a description of each directory.
Directory/filename |
Description |
---|---|
/contracts | Directory for Solidity contract files you want to compile and deploy. |
/contracts/Migrations.sol | Special contract that Truffle uses to deploy project contracts. |
/migrations | JavaScript configuration files to perform migrations (more on this later). |
/test | Truffle can automatically test both Solidity contract code and JavaScript application code through unit tests placed in this folder. |
truffle.js | The Truffle project configuration file for Linux or macOS. |
truffle-config.js | The Truffle project configuration file for Windows. |
If you’re running Windows, keep truffle-config.js and remove truffle.js. If you’re running on Linux or macOS, you can remove truffle-config.js and use truffle.js. In the rest of the chapter, I’ll refer to truffle.js—you’ll understand why shortly.
Copy the latest version of SimpleCoin from C:EthereummochaSimpleCoinSimpleCoin.sol to C:EthereumTruffleSimpleCoinContractsSimpleCoin.sol. Downgrade the pragma solidity instruction of SimpleCoin.sol to 0.4.23 after copying the file, as the latest version of Truffle at the time of writing uses solc 0.4.23.
You perform contract deployment in Truffle through so-called migrations, as I’ll explain shortly. You execute these through a Migrations contract, shown in the following listing, which was auto-generated in the contracts folder when you initialized the project with the truffle init command.
pragma solidity ^0.4.23; contract Migrations { address public owner; uint public last_completed_migration; constructor() public { owner = msg.sender; } modifier restricted() { if (msg.sender == owner) _; } function setCompleted(uint completed) public restricted { last_completed_migration = completed; } function upgrade(address new_address) public restricted { Migrations upgraded = Migrations(new_address); upgraded.setCompleted(last_completed_migration); } }
You can now kick the compilation
C:EthereumTruffleSimpleCoin>truffle compile
and you’ll see output similar to this:
Compiling .contractsSimpleCoin.sol... Writing artifacts to .uildcontracts
After a successful compilation, Truffle has created a new directory relative to the project folder, /build/contracts. The folder contains compilation artifacts, described in table 11.2, that you’ll use during the deployment stage.
Artifact |
Purpose |
---|---|
Migrations.json | ABI interface and bytecode of Migrations.sol |
SimpleCoin.json | ABI interface and bytecode of SimpleCoin.sol |
If you’ve started having issues at this point, it might be because you’re working in Windows or have a compiler versioning problem. You’ll need a solution so you can complete the work in this chapter. This two-part section provides some advice to help you solve such problems.
If you’re working in Windows, you might experience issues when running any Truffle command. Specifically, if you’re running Truffle compile in a Windows command shell, you might get the cryptic Microsoft JScript runtime error shown in figure 11.1.
The error occurs because Windows can’t distinguish correctly between the Truffle.cmd command file in the npm folder (typically in C:UsersYOURNAMEAppData Roaming pm) and the truffle.js file in your Truffle project folder. You have four options for solving this issue:
If you get compilation errors, it might be because you’re running an old version of the compiler or because Truffle is referencing an old version of the compiler, even if you’ve upgraded it recently. Specifically, after executing the truffle compile command, you might get some compilation error messages due to new constructor syntax introduced in solc 0.4.22. You have two options for fixing the issue. The best approach is to uninstall Truffle and reinstall it globally by entering
C:EthereumTruffleSimpleCoin>npm uninstall truffle -g
followed by
C:EthereumTruffleSimpleCoin>npm install [email protected] -g
Now if you re-execute truffle compile, you should get at most some warnings associated, possibly, with the Migrations contract (in the contractsMigrations.sol file). It might still be implemented under an old constructor convention, depending on the version of Truffle you’re running. If you’re running Truffle 4.1.15 or later, you shouldn’t have any issues; the Migrations.sol file should have been autogenerated like in listing 11.1. If you’re running an older version and want to get rid of the warnings, replace
constructor() public { owner = msg.sender; }
with
function Migrations() public { owner = msg.sender; }
If you recompile, you should no longer get any warnings:
C:Ethereum ruffleSimpleCoin>truffle compile Compiling .contractsMigrations.sol... Writing artifacts to .uildcontracts
The second way to fix compilation issues, which I’d leave as a last resort, is to reinstall solc, but only into the project folder:
C:EthereumTruffleSimpleCoin>npm install [email protected]
A migration is a deployment script. As seen before, migration scripts are in the migrations directory. Replace the content of 2_deploy_contracts.js with the script shown in the following listing.
var SimpleCoin = artifacts.require("SimpleCoin") 1 module.exports = function(deployer) { 2 deployer.deploy(SimpleCoin, 10000); 3 };
During the first migration execution, you also have to deploy the Migrations contract. This contract gets deployed through its own migration script, shown in the following listing and contained in this file: CEthereumTruffleSimpleCoinmigrations1_initial_migration.js.
var Migrations = artifacts.require(“Migrations”) module.exports = function(deployer) { deployer.deploy(Migrations); };
Once you’ve placed the migration scripts in the migrations directory, open a separate OS shell and move to the project folder:
C:>cd EthereumTruffleSimpleCoin
You’ll be deploying SimpleCoin onto Ganache, the mock Ethereum client you already saw in chapters 8 and 10. Ganache is part of the Truffle framework. If you haven’t already installed it, you can do so through npm, as usual:
C:Ethereum ruffleSimpleCoin>npm install -g [email protected]
Then, after the installation is complete, start it up:
C:>cd EthereumTruffleSimpleCoin>ganache-cli
Make sure truffle.js (or truffle-config.js on Windows) is configured to point to Truffle Develop, as shown in the following listing, as opposed to pointing to a public test network. Amend its content as shown in the following listing.
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id } } };
By default, your contracts will be deployed under Truffle from accounts[0], which is the default transaction sender. If you want the deployment transaction to be submitted from another account, you must specify the full address in the truffle.js file in the from property, as shown here:
module.exports = { networks: { development: { host: "localhost", port: 8545, from: "0xf17f52151ebef6c7334fad080c5704d77216b732", 1 network_id: "*" // Match any network id } } };
Go back to the previous console and run
CEthereumTruffleSimpleCoin>truffle migrate
Then you’ll get output like this:
Using network 'development'. Running migration: 1_initial_migration.js Deploying Migrations... ... 0x5823254426b34ec4220be899669e562d4691a72fa68fa1956a8eb87f9f431982 Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0 Saving successful migration to network... ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956 Saving artifacts... Running migration: 2_deploy_contracts.js Deploying SimpleCoin... ... 0x21c4120f9f231ea5563c2a988de55440139ba087651d3d292f06ae65434580f7 SimpleCoin: 0x345ca3e014aaf5dca488057592ee47305d9b3e10 Saving successful migration to network... ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0 Saving artifacts...
This shows that SimpleCoin has been deployed successfully on Ganache’s mock network. If you prefer to run the mock network client and Truffle commands from within the same OS shell, you can use a separate console called Truffle Develop. Have a look at the sidebar if you want to learn more. For the rest of the book, you’ll keep using two separate consoles: one to run Ganache and the other to launch Truffle commands.
You can start Truffle Develop from the Truffle project folder, as follows:
CEthereumTruffleSimpleCoin>truffle develop
This will start a mock network client similar to Ganache (and TestRPC), but on port 9545, which means, if you have Ganache running, you don’t need to shut it down.
Truffle Develop startup screen
But if you want to use Truffle Develop, you need to change the truffle.js configuration to point to port 9545:
module.exports = { networks: { development: { host: "localhost", port: 9545, network_id: "*" // Match any network id } } };
Now you’ll be able to run Truffle commands, such as migrate, directly from the Truffle Develop console:
Truffle(develop)>migrate
You’ll see exactly the same output as before.
Although in this chapter you’ll be deploying contracts only on Ganache (or TestRPC), at some point you might want to deploy on the public test network or production network. If so, you must modify your truffle.js (or truffle-config.js) file and include configurations for a test network and a live network, as follows:
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" }, live: { host: "localhost", port: 80, network_id: 1, }, ropsten: { host: "localhost", port: 80, network_id: 3, } }};
This configuration assumes you’re running a local geth node (the host is pointing to localhost), and the default Web 3 provider will be instantiated by Truffle as
new Web3.providers.HttpProvider("http://<host>:<port>")
But if you decide to use a different provider, for instance an HDWalletProvider pointing to a Ropsten Infura network, you must configure it explicitly with the provider property:
networks: { ... ropsten: { provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"), network_id: '3', }, ...
Also, bear in mind, when deploying on a public network, you might want to specify other settings, such as the gas limit, the gas price, and the account the contract should be deployed through. If so, you can add relevant configuration properties as described in table 11.3, but I invite you to consult the official documentation before proceeding further: http://mng.bz/mmgy.
Property | Purpose |
gas: | Gas limit (default = 4712388) |
gasPrice: | Gas price (default = 100,000,000,000 wei) |
from: | Sender address of the deployment transaction (and contract owner) |
provider: | The default is web3, as explained previously. |
Once you’ve configured truffle.js (or truffle-config.js) appropriately, you can kick a deployment to the Ropsten network as follows:
CEthereumTruffleSimpleCoin>truffle migrate --network ropsten
And, as you might have guessed, you’d deploy to MAINNET as follows:
CEthereumTruffleSimpleCoin>truffle migrate --network live
Truffle supports clean-room testing, which means that if a contract has been deployed on Ganache, its state will get reinitialized at the beginning of each test file being processed. If the contract has been deployed on a public network, the migrations will get re-executed at the start of each test file being processed, effectively redeploying the contract from scratch before the execution of the tests. In general, during development it’s preferable to run unit tests against Test Develop, as they run up to 90% faster. It’s advisable to run the tests on a private network, and ultimately on a public network, in later stages of the development cycle to make sure you’ve tested application aspects of communicating with a real network.
It’s possible to write two different classes of tests with Truffle:
You must place all test scripts, whether written in Solidity or JavaScript, in the test directory of the Truffle project. We’ll focus almost entirely on JavaScript tests, as Solidity tests are meant to test contract-to-contract interaction, which is a more advanced topic outside the scope of this book. But I’ll give you some of the basics of working with Solidity tests.
You implement Solidity tests through custom test contracts whose code you have to place in .sol files within the project test directory. Solidity tests must be able to run against
You structure a Solidity test contract the way TestSimpleCoin.sol does, as shown in listing 11.5. You must follow these guidelines and conventions:
pragma solidity ^0.4.2; import "Truffle/Assert.sol"; 1 import "Truffle/DeployedAddresses.sol"; 2 import "../contracts/SimpleCoin.sol"; 3 contract TestSimpleCoin { 4 function testInitialBalanceUsingDeployedContract() public { SimpleCoin simpleCoin = SimpleCoin( DeployedAddresses.SimpleCoin()); 5 uint expected = 10000; Assert.equal(simpleCoin.coinBalance(tx.origin), expected, "Owner should have 10000 SimpleCoin initially"); 6 } }
First, place TestSimpleCoin.sol in the test folder and make sure Test Develop (or TestRPC) is still running. (If it’s not running, restart it in a separate console.) You can then run Solidity tests by executing this command:
CEthereumTruffleSimpleCoin>truffle test
You’ll get output similar to this:
Using network 'development'. Compiling .contractsSimpleCoin.sol... Compiling . estTestSimpleCoin.sol... Compiling Truffle/Assert.sol... Compiling Truffle/DeployedAddresses.sol... TestSimpleCoin √ testInitialBalanceUsingDeployedContract (48ms) 1 passing (530ms)
Depending on the version of your solc compiler, you might get some warnings, especially around code raising events.
As I mentioned earlier, contract-to-contract testing is an advanced topic outside of the scope of this book, so I won’t cover Solidity tests further. The next section will be dedicated entirely to JavaScript tests under Truffle.
Do you remember the effort you put into writing tests in Mocha? It wasn’t wasted: Truffle supports Mocha JavaScript testing, but with deeper integration with Ethereum. This means you
To run in Truffle the JavaScript tests you wrote earlier in Mocha, you’ll have to make minor changes, especially if you want to ensure they’re executed in clean-room mode:
I’ll demonstrate here how to rewrite some of the tests, so you can get a better idea of what the guidelines I’ve given mean. I’ll start from the first test against SimpleCoin’s constructor. For convenience, I’ve repeated the code from the original test you wrote in Mocha in the following listing, so you don’t have to flip pages back and forth.
var assert = require('assert'); const source = fs.readFileSync('c:/Ethereum/mocha/SimpleCoin/SimpleCoin.sol', 'utf8'); const compiledContract = solc.compile(source, 1); const abi = compiledContract.contracts[':SimpleCoin'].interface; const bytecode = '0x' + compiledContract.contracts[':SimpleCoin'].bytecode; const gasEstimate = web3.eth.estimateGas({ data: bytecode }) + 100000; const SimpleCoinContractFactory = web3.eth.contract(JSON.parse(abi)); describe('SimpleCoin', function() { this.timeout(5000); describe('SimpleCoin constructor', function() { it('Contract owner is sender', function(done) { //arrange let sender = web3.eth.accounts[1]; let initialSupply = 10000; //act let simpleCoinInstance = SimpleCoinContractFactory.new(initialSupply, { from: sender, data: bytecode, gas: gasEstimate}, function (e, contract){ if (typeof contract.address !== 'undefined') { //assert assert.equal(contract.owner(), sender); done(); } }); }); }); });
Now create a file called testSimpleCoin.js and place it in the test directory. Fill it with the code shown in the following listing.
const SimpleCoin = artifacts.require( "./SimpleCoin.sol"); 1 contract('SimpleCoin', function(accounts) { 2 contract('SimpleCoin.Constructor', function(accounts) { 3 it("Contract owner is sender", function() { 4 return SimpleCoin.deployed() 5 .then(function(instance) { 6 return instance.owner(); 7 }) .then(function(contractOwner) { 8 assert.equal(contractOwner.valueOf(), accounts[0], 9 "accounts[0] wasn't the contract owner"); 10 }); }); }); });
As I mentioned in the deployment section, by default, contracts are deployed under Truffle from accounts[0]. That’s why in the test you’re comparing the address of the contract owner, contractOwner.valueOf(), with accounts[0].
You’ve surely noticed a few differences between Mocha and Truffle tests, such as those listed in table 11.4.
Mocha tests |
Truffle tests |
---|---|
Mocha’s test script starts with a relatively long setup, during which we go through all the necessary low-level steps required for deploying SimpleCoin. | Truffle’s test has hardly any setup at all and immediately references a deployed instance of SimpleCoin (through the migration framework). |
Mocha’s test execution is based on a callback associated with the deployment of a SimpleCoin instance. | Truffle’s test execution is based on a chain of promises starting with a promise of a deployed SimpleCoin instance. |
Once you get ahold of a SimpleCoin instance (or a promise of it), Mocha’s test code seems shorter and to the point. | After getting a SimpleCoin instance, Truffle’s code goes through various steps to get ahold of the contract owner for subsequently comparing it with the expected one. |
In summary, thanks to Truffle’s migration framework, tests don’t need much setup to access an instance of SimpleCoin. On the other hand, after referencing a SimpleCoin instance, Mocha’s test seems less verbose than Truffle’s.
Let’s run the test! When you re-execute the Truffle test command, both Solidity and JavaScript tests will be run, and you’ll get output similar to this:
C:EthereumTruffleSimpleCoin>Truffle test Using network 'development'. Compiling .contractsSimpleCoin.sol... Compiling . estTestSimpleCoin.sol... Compiling Truffle/Assert.sol... Compiling Truffle/DeployedAddresses.sol... TestSimpleCoin √ testInitialBalanceUsingDeployedContract (49ms) Contract: SimpleCoin Contract: SimpleCoin.Constructor √ Contract owner is sender 2 passing (1s)
You can rewrite the constructor test that verifies the owner balance is the initial supply. Add the following it() block in testSimpleCoin.js:
it("Contract owner balance is equal to initialSupply", function() { return SimpleCoin.deployed() .then(function(instance) { return instance.coinBalance(accounts[0]); }).then(function(contractOwnerBalance) { assert.equal(contractOwnerBalance.valueOf(), 10000, "the contract owner balance is not equal to the full supply of 10000"); }); });
As you might remember, SimpleCoin’s migration script (named 2_deploy_contracts.js and shown in listing 11.2) sets the initial supply to 10,000. That’s why the contract owner balance is being compared to 10,000.
Rerun the tests:
C:EthereumTruffleSimpleCoin>Truffle test
TestSimpleCoin √ testInitialBalanceUsingDeployedContract (49ms) Contract: SimpleCoin Contract: SimpleCoin.Constructor √ Contract owner is sender √ Contract owner balance is equal to initialSupply 3 passing (1s)
In the file named testSimpleCoin_ALL_sync.js provided on the book website, you can find a full Truffle test suite equivalent to the test suite you wrote earlier in Mocha. I encourage you to go through the tests in detail and compare the related tests.
You’ll come to the conclusion that the ideal structure for a test is a sort of chimera combining the easy setup of Truffle tests and the simple and direct code within Mocha’s callbacks. There is indeed a way to achieve this chimera, and it’s through JavaScript’s async/await syntax.
JavaScript’s async/await syntax allows you to perform asynchronous processing through syntax that resembles that of synchronous programming—much simpler than that of typical asynchronous programming techniques such as callbacks or promise chains. If you’re interested in learning more about JavaScript asynchronous coding, I recommend Secrets of the Java-Script Ninja by John Resig, et al, published by Manning.
To take advantage of async/await, you must be running on Node.js version 8.0 or higher. I also advise you to install Truffle version 4.0 or higher.
Here’s how the first constructor test, which verifies contract ownership, looks using async/await:
const SimpleCoin = artifacts.require("./SimpleCoin.sol"); contract('SimpleCoin', function(accounts) { contract('SimpleCoin.Constructor', function(accounts) { it("Contract owner is sender", async function() { let simpleCoinInstance = await SimpleCoin.deployed(); 1 let contractOwner = await simpleCoinInstance.owner(); 2 assert.equal(contractOwner.valueOf(), accounts[0], "accounts[0] wasn't the contract owner"); 3 }); }); });
As you can see, by replacing the promise chain of the initial test version with statements based on async/await, the code looks as simple as a plain synchronous implementation would look. You’ve achieved exactly what you were looking for:
Place this test in a new file—for example, called testSimpleCoin_asyncawait.js. Put it in the test folder and run it as usual (removing testSimpleCoin.js and TestSimpleCoin.sol from the test folder before doing so, so they don’t get executed):
C:EthereumTruffleSimpleCoin>truffle test
You will get this output:
Using network 'development'. Contract: SimpleCoin Contract: SimpleCoin.Constructor √ Contract owner is sender 1 passing (53ms)
I challenge you to convert the second constructor test, which verifies the owner balance is the initial supply, from the earlier version based on a promise chain to async/await statements. Look away and write your implementation before comparing your solution with mine!
Have you finished? Does your test look similar to this?
it("Contract owner balance is equal to initialSupply", async function() { let simpleCoinInstance = await SimpleCoin.deployed(); let contractOwnerBalance = await simpleCoinInstance.coinBalance(accounts[0]); assert.equal(contractOwnerBalance.valueOf(), 10000, "the contract owner balance is not equal to the full supply of 10000"); });
If you’re still unconvinced of the benefits of moving from tests based on promise chains to tests based on async/await, I’ll show you a more dramatic comparison. Here’s the chain-promise-based version of a test on a successful transfer() operation:
contract('SimpleCoin.transfer', function(accounts) { it("Succesful transfer: final sender and recipient balances are correct", function() { //arrange let sender = web3.eth.accounts[0]; let recipient = web3.eth.accounts[1]; let tokensToTransfer = 200; const expectedSenderBalance = 9800; const expectedRecipientBalance = 200; //act return SimpleCoin.deployed() 1 then(function(instance) { simpleCoin = instance; return simpleCoin.transfer(recipient, tokensToTransfer, {from: sender}); 2 }).then(function() { return simpleCoin.coinBalance(sender); 3 }).then(function(balance) { sender_ending_balance = balance.toNumber(); 4 return simpleCoin.coinBalance(recipient); 5 }).then(function(balance) { recipient_ending_balance = balance.toNumber(); 6 //assert assert.equal(sender_ending_balance, expectedSenderBalance, "Amount wasn't correctly taken from the sender"); assert.equal(recipient_ending_balance, expectedRecipientBalance, "Amount wasn't correctly sent to the receiver"); }); }); });
As you can see, having to reference balances through promises before assigning them to variables makes the test rather convoluted. Here’s the equivalent async/await version of this test:
contract('SimpleCoin.transfer', function(accounts) { it("Succesful transfer: final sender and recipient balances are correct", async function() { //arrange let sender = web3.eth.accounts[0]; let recipient = web3.eth.accounts[1]; let tokensToTransfer = 200; const expectedSenderBalance = 9800; const expectedRecipientBalance = 200; let simpleCoinInstance = await SimpleCoin.deployed(); //act await simpleCoinInstance.transfer(recipient, tokensToTransfer, {from: sender}); let sender_ending_balance = await simpleCoinInstance.coinBalance(sender); let recipient_ending_balance = await simpleCoinInstance.coinBalance(recipient); //assert assert.equal(sender_ending_balance.valueOf(), expectedSenderBalance, "Amount wasn't correctly taken from the sender"); assert.equal(recipient_ending_balance.valueOf(), expectedRecipientBalance, "Amount wasn't correctly sent to the receiver"); }); });
Isn’t this clearer? I’m so confident you can understand this version of the test without any explanation that I decided I didn’t need to annotate the code at all. If you have Node.js 8 (or higher) installed, you should definitely consider writing your tests with async/await rather than promise chains. I encourage you to try and convert all the chain-promise-based Truffle tests you find in the testSimpleCoin_ALL_sync.js file to equivalent async/await ones.