In previous chapters, you learned how to develop an Ethereum Dapp by using simple tools that the platform offers. You were able, with some effort, to build an end-to-end Dapp, including a simple web UI and a smart contract layer (including libraries) and to deploy it to the public test network. You wrote Solidity and Web3.js code through Remix or even through plain text editors and launched simple deployment scripts manually, initially on the geth console and later on Node.js.
Although this way of developing a Dapp is acceptable while learning, it isn’t efficient when you start to dedicate more time to developing decentralized applications, especially if you do it professionally. As you learned in the chapter dedicated to the Ethereum ecosystem, various third-party development tools are available to improve code editing to help you unit test your contracts and speed up the development cycle.
In this chapter, I’ll present Mocha, a JavaScript unit testing framework that will allow you to easily automate unit tests for your contracts. In the next chapter, you’ll learn how to set up Truffle, through which you’ll automate build and deployment of your Dapps. Finally, you’ll also incorporate Mocha tests within Truffle, making it your fully integrated development environment.
You’ll learn Mocha by writing a unit test suite for SimpleCoin. Before you start, I’ll show you briefly how to install the framework and set up the working directory.
Mocha is executed through Node.js, so you have to install it using npm. Because you’ll be writing tests for various smart contracts, it’s best to install it globally. (You might have to Run it as an Administrator, depending on your security configuration.) Here’s how to install it:
c:>npm install --global mocha
That’s it! The next step is to prepare a working directory for your SimpleCoin tests.
You should place tests against an Ethereum smart contract in a working directory configured for Node.js and set up with Ethereum packages such as Web3 and Ganache. Create a working directory for the SimpleCoin unit tests you’re going to write. I’ve created mine as C:EthereummochaSimpleCoin.
Now create a package.json configuration file for Node.js in this folder and set the test script to Mocha, as shown in the following listing.
{ "name": "simple_coin_tests", "version": "1.0.0", "description": "unit tests for simple coin", "scripts": { "test": "mocha" } }
You can also create it interactively by opening an OS shell, moving to the working directory you’ve created, and running
C:EthereummochaSimpleCoin>npm init
Once you have the directory and configuration file created, you can quickly try out how Mocha executes tests. Create a dummyTests.js file containing the example test shown in the following listing.
var assert = require('assert'); 1 describe('String', function() { 2 describe('#length()', function() { 3 it('the length of the string "Ethereum" should be 8', function() { 4 assert.equal(8, 'Ethereum'.length); 5 }); }); });
You can run this test file as follows:
C:EthereummochaSimpleCoin>npm test dummyTests.js
And you’ll get this output:
> [email protected] test C:EthereummochaSimpleCoin > mocha "dummyTests.js" String #length() √the length of the string "Ethereum" should be 8 1 passing (9ms)
As you may have noticed, the assert library referenced at the top of the test script is the default assert library that the framework provides. If you want, you can install and then reference any of the following assert frameworks:
The testing working directory is almost ready. Before you can use it for testing smart contracts, you must install the following node packages: solc, Web3, and Ganache. If you haven’t installed them globally already, you can do so as follows:
C:>npm install -g solc C:>npm install -g [email protected] C:>npm install -g [email protected]
Alternatively, you can install these packages only in the testing folder as follows:
C:EthereummochaSimpleCoin>npm install solc C:EthereummochaSimpleCoin>npm install [email protected] C:EthereummochaSimpleCoin>npm install [email protected]
You’re all set up to write some tests against SimpleCoin. Now I can show you how to unit test a solidity contract in Mocha. This isn’t meant to be a tutorial on unit testing, though, and I’ll assume you have basic knowledge of or experience in unit testing. If you don’t know anything about unit testing but wish to learn more on the topic, I can recommend two excellent books that will give you a solid foundation: Effective Unit Testing by Lasse Koskela and The Art of Unit Testing by Michael Feathers and Robert C. Martin, both published by Manning.
You’ll be creating tests against the functionality offered by the extended version of SimpleCoin you implemented back in chapter 5. I’ve repeated it in the next listing for your convenience, and you can place it in the following file: c:EthereummochaSimpleCoinSimpleCoin.sol.
pragma solidity ^0.4.24; contract SimpleCoin { mapping (address => uint256) public coinBalance; mapping (address => mapping (address => uint256)) public allowance; mapping (address => bool) public frozenAccount; address public owner; event Transfer(address indexed from, address indexed to, uint256 value); event FrozenAccount(address target, bool frozen); modifier onlyOwner { require(msg.sender == owner); _; } constructor(uint256 _initialSupply) public { owner = msg.sender; mint(owner, _initialSupply); } function transfer(address _to, uint256 _amount) public { require(coinBalance[msg.sender] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); coinBalance[msg.sender] -= _amount; coinBalance[_to] += _amount; emit Transfer(msg.sender, _to, _amount); } function authorize(address _authorizedAccount, uint256 _allowance) public returns (bool success) { allowance[msg.sender][_authorizedAccount] = _allowance; return true; } function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) { require(_to != 0x0); require(coinBalance[_from] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); require(_amount <= allowance[_from][msg.sender]); coinBalance[_from] -= _amount; coinBalance[_to] += _amount; allowance[_from][msg.sender] -= _amount; emit Transfer(_from, _to, _amount); return true; } function mint(address _recipient, uint256 _mintedAmount) public onlyOwner { coinBalance[_recipient] += _mintedAmount; emit Transfer(owner, _recipient, _mintedAmount); } function freezeAccount(address target, bool freeze) public onlyOwner { frozenAccount[target] = freeze; emit FrozenAccount(target, freeze); } }
Unit testing a Solidity contract means verifying that all the public methods the contract exposes, including the contract constructor, behave as expected, with both valid and invalid input. First, I’ll help you verify that the constructor initializes the contract as expected. You’ll do so by correctly setting the contract owner value and the initial state according to the account used to execute the deployment transaction and the values fed to the constructor parameters.
Then I’ll show you a set of tests for the typical negative and positive checks you want to perform against any contract function. By positive checks, I mean those verifying successful logic execution: a contract function invoked by an authorized user and fed with valid input within the constraints defined by all modifiers decorating the function and acceptable to the function logic executes successfully.
By negative checks I mean those verifying expected exceptions are thrown for an unauthorized caller or invalid input:
Once you’re familiar with these types of tests, you’ll be able to write tests against any contract function. You can start by testing SimpleCoin’s constructor. While we look into that, I’ll also give you a general idea of how to initialize and structure your tests.
As you know, testing a piece of code means executing the code under test and then verifying assumptions about what should have happened. You execute the code of the contract constructor only during its deployment, so your test must deploy SimpleCoin and then verify that the construction was executed correctly, specifically by checking the following:
Unit tests must generally run as quickly as possible, because you’re likely to execute them many times through your development cycle. In the case of enterprise applications, the main source of latency comes from accessing environmental resources, such as the file system, databases, and network. The most common way of reducing or eliminating such latency is to emulate access to these resources with isolation (or mocking) frameworks, such as jMock and EasyMock for Java applications and Moq, NMock, and Rhino Mocks for .net applications.
When it comes to Ethereum Dapps, the main source of latency is transaction processing (including mining and block creation) and block propagation throughout the Ethereum network. As a result, it’s natural to run contract unit tests against a mock network such as Ganache, which emulates the infrastructural aspects of the Ethereum platform without connecting to it.
This means your first test, against SimpleCoin’s constructor, must deploy SimpleCoin on Ganache before performing the functional checks I’ve described (of contract ownership and the account balance of the owner). In fact, to ensure no interferences occur between tests, each test will redeploy SimpleCoin from scratch.
Deploying SimpleCoin onto Ganache... Hold on—you already did this in chapter 8! You can adapt listing 8.5 for unit testing. You can keep the script almost unchanged up to the instantiation of SimpleCoinContractFactory. The only modification is the path of SimpleCoin.sol, which is now c:EthereummochaSimpleCoin. Here’s the script:
const fs = require('fs'); const solc = require('solc'); const Web3 = require('web3'); const web3 = new Web3( new Web3.providers.HttpProvider("http://localhost:8545")); var assert = require('assert'); const source = fs.readFileSync( 'c:/Ethereum/mocha/SimpleCoin/SimpleCoin.sol', 'utf8'); 1 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));
Now that you’ve taken care of the infrastructural (nonfunctional) part of your first test, you can focus on the functional one. Bear in mind, though, that these initial lines of the script haven’t deployed SimpleCoin yet; they’ve merely instantiated the contract factory.
Following the pattern in the mock test from listing 10.2, you can start to document the purpose of your first test through Mocha’s describe() and it() statements:
describe('SimpleCoin', function() { 1 describe('SimpleCoin constructor', function() { 2 it('Contract owner is sender', function(done) { 3 ... }); }); });
It’s time to write the core of your first test, which will, as I stated earlier, check that the contract owner is the same account as the sender of the deployment transaction. You can structure the test with an AAA layout, which includes the following three parts, as also illustrated in figure 10.1:
describe('SimpleCoin', function() { this.timeout(5000); describe('SimpleCoin constructor', function() { it('Contract owner is sender', function(done) { //arrange let sender = web3.eth.accounts[1]; 1 let initialSupply = 10000; 1 //act let simpleCoinInstance = SimpleCoinContractFactory.new(initialSupply, { 2 from: sender, data: bytecode, gas: gasEstimate}, function (e, contract){ if (typeof contract.address !== 'undefined') { //assert assert.equal(contract.owner(), sender); 3 done(); 4 } }); }); }); });
You’re now ready to run your first unit test, which is shown in its entirety in the following listing. You can place this script in the following file: c:EthereummochaSimpleCoinSimpleCoinTests.js.
const fs = require('fs'); const solc = require('solc'); const Web3 = require('web3'); const web3 = new Web3( new Web3.providers.HttpProvider("http://localhost:8545")); 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(); } }); }); }); });
Before running the script, open a new console and start Ganache:
c:>ganache-cli
Now go back to the console from which you executed the dummy test earlier and run your new test script:
C:EthereummochaSimpleCoin>npm test SimpleCoinTests.js
You’ll see output like what’s shown in figure 10.2.
The test is passing, which means the contract owner is indeed the sender of the deployment transaction. Good news! You can move on to the next test.
Before leaving the constructor, you should test whether the balance of the contract owner is equal to the initial supply fed with the initialSupply parameter. Add the following it() block within the describe() section associated with the SimpleCoin constructor:
it('Contract owner balance is equal to initialSupply', 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.coinBalance(contract.owner()), initialSupply); 1 done(); } }); });
You might be wondering why I didn’t add the same assert line to the previous test. In general, it’s good practice to keep each unit test focused on one specific thing. Given that this test has nothing to do with verifying contract ownership, I decided to create a completely separate test. As I mentioned earlier, you also should completely isolate every test from other tests to avoid cross-dependencies and side effects that might invalidate unrelated tests. That’s why you should redeploy SimpleCoin at each test—by doing so, you can be confident that the test is truly isolated.
Now rerun the test script:
C:EthereummochaSimpleCoin>npm test SimpleCoinTests.js
You can see from the output in figure 10.3 that both tests have passed. Also, if you look at the Ganache console, you can verify that while running this test session, SimpleCoin was indeed deployed twice—once for each test, as shown in figure 10.4.
We’ll now move to the set of tests you typically want to write against each contract function. If you look at listing 10.3, you’ll notice both mint() and freezeAccount() restrict their execution to the contract owner through the onlyOwner modifier:
function mint(address _recipient, uint256 _mintedAmount) onlyOwner public { ... function freezeAccount(address target, bool freeze) onlyOwner public {
A test you should write for each of these functions is to verify that an exception is thrown if you try to call them from an account that isn’t the contract owner. Here’s how you write such a test for mint():
describe('mint', function() { it('Cannot mint from non-owner account', function(done) { //arrange let sender = web3.eth.accounts[1]; 1 let initialSupply = 10000; let minter = web3.eth.accounts[2]; 2 let recipient = web3.eth.accounts[3]; let mintedCoins = 3000; let simpleCoinInstance = simpleCoinContractFactory .new(initialSupply, { from: sender, data: bytecode, gas: gasEstimate}, 1 function (e, contract){ if (typeof contract.address !== 'undefined') { //act and assert assert.throws( 3 ()=> { contract.mint(recipient, mintedCoins, {from:minter, gas:200000}); 2 }, /VM Exception while processing transaction/ ); done(); } }); }); });
As you can see, you verify that an exception is thrown when calling a function by wrapping it with the following assert statement:
assert.throws( ()=> contract.functionBeingTested(), /Expected exception/ );
You’ll use this technique several times in upcoming sections.
Even when the caller is authorized to invoke a function, they must feed it valid input. You should verify that if they don’t do so, an exception is thrown for any breach to function modifiers or require conditions.
Recall that the transfer() function performs input validation through various require statements before executing the token transfer:
function transfer(address _to, uint256 _amount) public { require(_to != 0x0); require(coinBalance[msg.sender] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); coinBalance[msg.sender] -= _amount; coinBalance[_to] += _amount; Transfer(msg.sender, _to, _amount); }
Ideally, you should write a test for each of the require statements. I’ll show you how to write a test against the second require statement:
require(coinBalance[msg.sender] > _amount);
This constraint prevents the sender from sending more tokens than they own. If they try to do so, an exception is thrown. You can verify this is happening by using the same assert.throws statement you saw earlier:
describe('transfer', function() { it('Cannot transfer a number of tokens higher than that of tokens owned', function(done) { //arrange let sender = web3.eth.accounts[1]; let initialSupply = 10000; let recipient = web3.eth.accounts[2]; let tokensToTransfer = 12000; 1 let simpleCoinInstance = SimpleCoinContractFactory.new(initialSupply, { from: sender, data: bytecode, gas: gasEstimate}, function (e, contract){ if (typeof contract.address !== 'undefined') { //act and assert assert.throws( 2 ()=>{ contract.transfer(recipient, tokensToTransfer, { from:sender, gas:200000}); }, /VM Exception while processing transaction/ 3 ); done(); } }); }); });
The transfer() function has two other require statements. I encourage you to write similar tests for them.
After you write tests performing negative checks so you’re confident that no unauthorized accounts or accounts with invalid input can call the function, it’s time to write a positive test proving the logic performs successfully when an authorized account invokes the function and feeds it with valid input. As an example, you could write a new test against the transfer() function dealing with a successful token transfer. In this case, you must verify that the sender account balance has decreased by the transferred amount, whereas the receiving account has increased by the same amount:
it('Successful transfer: final sender and recipient balances are correct', function(done) { //arrange let sender = web3.eth.accounts[1]; let initialSupply = 10000; let recipient = web3.eth.accounts[2]; let tokensToTransfer = 200; 1 let simpleCoinInstance = SimpleCoinContractFactory.new(initialSupply, { from: sender, data: bytecode, gas: gasEstimate}, function (e, contract){ if (typeof contract.address !== 'undefined') { //act contract.transfer(recipient, tokensToTransfer, { from:sender,gas:200000}); //assert const expectedSenderBalance = 9800; 2 const expectedRecipientBalance = 200; 2 let actualSenderBalance = contract.coinBalance(sender); 3 let actualRecipientBalance = contract.coinBalance(recipient); 3 assert.equal(actualSenderBalance, expectedSenderBalance); 4 assert.equal(actualRecipientBalance, expectedRecipientBalance); 4 done(); } }); });
Add the two tests you’ve written against transfer() to the test script you started to write earlier against the constructor and rerun it:
C:EthereummochaSimpleCoin>npm test SimpleCoinTests.js
As you can see in figure 10.5, the test output now shows two sections: one for the constructor tests and the other for the transfer() tests. All tests are passing.
As an additional test to perform a positive check, you could write a test against the authorize() function, which has no modifiers and no input validation:
function authorize(address _authorizedAccount, uint256 _allowance) public returns (bool success) { allowance[msg.sender][_authorizedAccount] = _allowance; return true; }
The most obvious test to write is therefore one that verifies that the allowance set is the expected one:
describe('authorize', function() { it('Successful authorization: the allowance of the authorized account is set correctly', function(done) { //arrange let sender = web3.eth.accounts[1]; let initialSupply = 10000; let authorizer = web3.eth.accounts[2]; let authorized = web3.eth.accounts[3]; let allowance = 300; 1 let simpleCoinInstance = SimpleCoinContractFactory.new( initialSupply, { from: sender, data: bytecode, gas: gasEstimate}, function (e, contract){ if (typeof contract.address !== 'undefined') { //act let result = contract.authorize(authorized, allowance, { from:authorizer,gas:200000}); 2 //assert assert.equal(contract.allowance(authorizer, authorized), 300); 3 done(); } }); }); });
Now that we’ve covered all the typical tests, I invite you to refresh your memory on the transferFrom() function. That function allows an account to transfer an amount from another account within an allowance previously authorized by the account owner:
function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) { require(_to != 0x0); require(coinBalance[_from] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); require(_amount <= allowance[_from][msg.sender]); coinBalance[_from] -= _amount; coinBalance[_to] += _amount; allowance[_from][msg.sender] -= _amount; Transfer(_from, _to, _amount); return true; }
Looking at its code, you might want to test at least these four scenarios:
You might have noticed these tests are similar to ones you’ve already written, so I won’t repeat myself here. But I encourage you to give these tests a shot and then compare your tests with mine, which you can find in listing C.1 in appendix C.
You can see all tests, including the ones I’ve skipped, in listing C.1 of appendix C. You can also find them in the SimpleCoinTest.js file of the provided code. After adding all these tests to SimpleCoinTests.js, you can run the whole suite, as follows:
C:EthereummochaSimpleCoin>npm test SimpleCoinTests.js
The output in figure 10.6 shows the tests nicely grouped in sections ... and all passing.
This section has given you an idea of the typical tests you might want to write against a contract you’re developing. You can find a summary in table 10.1.
Function |
Test |
Purpose |
---|---|---|
Constructor | Contract owner is sender | Testing contract ownership |
Constructor | Owner balance = supply | Testing correct state set from constructor parameters |
Mint | Can’t mint from nonowner account | Testing if an exception is raised when an unauthorized account invokes the function |
Transfer | Can’t transfer more tokens than owned | Testing if an exception is raised by invalid input breaching modifiers or require conditions |
Transfer | Successful transfer | Testing contract state following successful transaction executed from valid account and with valid input |
The test suite I’ve presented covers the most obvious test cases, but it’s by no means comprehensive. Like programming, unit testing is an art, not an exact science. You must always keep in mind the trade-off between coverage (Are all the functions of your contracts covered by tests? Are all logic branches covered by tests?) and accuracy (Are all boundary conditions of each function tested?) of your tests on one side and their cost (for implementation and maintenance) on the other side. Ideally, you might want to have maximum coverage and accuracy, but you might not have enough time and resources to implement and maintain all the necessary tests. In that case, you might want to focus on critical areas, especially on functionality for which Ether’s at stake.