Chapter 10. Unit testing contracts with Mocha

This chapter covers

  • Installing and setting up Mocha, a JavaScript unit testing framework
  • Unit testing SimpleCoin with Mocha
  • Writing tests performing negative checks, verifying expected exceptions are thrown
  • Writing tests performing positive checks, verifying logic is executed successfully

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.

10.1. Installing Mocha

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.

10.2. Setting up SimpleCoin in Mocha

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.

Listing 10.1. package.json
{
  "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.

Listing 10.2. dummyTests.js
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
    });
  });
});

  • 1 Imports the default assert library
  • 2 Names the test suite
  • 3 Names the functionality being tested
  • 4 Describes the specific test
  • 5 Actual test

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]

10.3. Writing unit tests for SimpleCoin

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.

Listing 10.3. SimpleCoin.sol, latest code from chapter 5
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);
    }
}

10.3.1. The testing plan

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:

  • A contract function that is restricted to certain callers through a modifier throws an expected exception if the caller isn’t authorized to invoke it.
  • A contract function that receives input that doesn’t meet other constraints defined by additional modifiers or require conditions throws an expected exception.

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.

10.3.2. Unit testing the constructor

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:

  • The contract owner is the same account as the sender of the deployment transaction.
  • The token balance of the contract owner is the same as the initial supply amount being fed to the constructor.

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));

  • 1 The location of simplecoin.sol is now in c:/Ethereum/ mocha/SimpleCoin.

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
    ...
    });
  });
});

  • 1 Describes the entire testing suite you’re creating
  • 2 Describes your first testing section, focused on SimpleCoin’s constructor
  • 3 Describes your first test

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:

  • Arrange—Sets up the input passed to the function under test and instantiates objects required for the testing
  • Act—Calls the function under test
  • Assert—Verifies the test assumptions
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
              }
        });
    });
 });
});

  • 1 Inputs to this test are the sender of the deployment transaction and the initial supply amount.
  • 2 Triggers the function under test, SimpleCoin’s constructor, through contract deployment
  • 3 Following successful deployment, verifies the contract owner is the sender of the transaction
  • 4 Signals to Mocha the completion of the test
Figure 10.1. The typical AAA structure of a unit test: Arrange (set up test input and the object under test); Act (call the function under test); and Assert (verify the test expected outcome)

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.

Listing 10.4. SimpleCoinTests.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.

Figure 10.2. Output of your first Mocha test, showing the name of the test suite, the name of the test section, and a description of your individual test. The test is passing!

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();
                }
     });
});

  • 1 This is the only line differing from the previous test. It verifies whether the balance of the contract owner equals the initial supply.
Note

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.

Figure 10.3. Amended test suite, including two constructor tests. Both have passed.

Figure 10.4. Ganache output during test execution. SimpleCoin is redeployed at each test execution.

10.3.3. Testing whether only authorized callers can invoke a function

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();
                    }
            });
     });
});

  • 1 Sender of the contract transaction (the contract owner)
  • 2 The account calling mint() isn’t the contract owner.
  • 3 Verifies an exception is thrown when mint() is called, because the caller of mint() isn’t the contract owner

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.

10.3.4. Testing if input constraints are met

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();
          }
       });
    });
});

  • 1 Sets an amount to be transferred that’s higher than the current balance
  • 2 Verifies that an exception is thrown
  • 3 Expected exception

The transfer() function has two other require statements. I encourage you to write similar tests for them.

10.3.5. Testing invocation from an authorized account with valid input

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();
        }
     });
});

  • 1 Sets amount to be transferred
  • 2 Expected sender and recipient balances after the transfer
  • 3 Actual sender and recipient balances after the transfer
  • 4 Verifies actual sender and recipient balances equal the expected ones

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.

Figure 10.5. Output of the amended test suite also including tests on the transfer() function. You can now see 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();
                    }
            });
    });
 });

  • 1 Sets the allowance amount
  • 2 Authorizes the account to use the specified allowance
  • 3 Verifies the allowance allocated to the authorized account is the expected one

10.3.6. A little challenge

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:

  • The authorized account can’t transfer a number of tokens higher than that owned by the authorizer.
  • An account can’t transfer tokens from an account that hasn’t authorized any allowance to any account.
  • An account can’t transfer tokens from an account that hasn’t authorized it any allowance.
  • An authorized account can transfer an amount within the allowance, the final balance of the authorizer has decreased by the amount transferred by the authorized account, and the balance of the recipient has increased by the same amount.

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.

10.3.7. The full testing suite

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.

Figure 10.6. Running the whole test suite. The output shows tests are grouped in sections ... and they’re 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.

Table 10.1. Purpose of the tests presented in this section

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.

Summary

  • You can write Ethereum contract unit tests relatively easily with Mocha, a generic JavaScript testing framework.
  • You can install Mocha quickly with npm, the Node.js package manager.
  • You can describe Mocha unit test packages and groups using describe() and individual tests using it().
  • Your tests should cover negative checks, verifying expected exceptions are thrown in case the function is invoked from an unauthorized account or with invalid input.
  • Your tests should cover positive checks, verifying the contract state has been successfully modified by the function logic when the function is called from an authorized account and with valid input.
  • You also should write tests against the constructor to verify that the contract owner and the contract state are initialized correctly.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset