Test and Deploy with Truffle - cogeorg/teaching GitHub Wiki

Using JavaScript as a testing framework will let you test your smart contract by simulating external client interactions. For that purpose, truffle integrates Mocha as JavaScript testing framework and Chai as a behavior driven development assertion library.

Recall the ExampleCoin contract.

pragma solidity ^0.5.0;

contract ExampleCoin {

    address public minter;
    mapping (address => uint) public balances;

    event Sent(address from, address to, uint amount);

    constructor() public {
        minter = msg.sender;
    }

    function mint(address receiver, uint amount) public {
        require(msg.sender == minter, "Function can only be called by minter.");
        require(amount < 1e60, "Amount is too high.");
        balances[receiver] += amount;
    }

    function send(address receiver, uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

Note that this coin does not follow any ERC standard (see e.g. here). Let us create a truffle project for the contract written above:

$ mkdir ExampleCoin
$ cd ExampleCoin/
$ truffle init

Create a file called ExampleCoin.sol inside the contracts/ folder and copy the above defined contract to that file. Also, create a file called 2_deploy_contracts.js in the migrations/ folder and paste the following code:

var ExampleCoin = artifacts.require('./ExampleCoin.sol')

module.exports = function (deployer) {
  deployer.deploy(ExampleCoin)
}

To recap the Truffle folder structure, have a look here.

Testing

In the test/ folder, create a file called ExampleCoin.js. We want to test the following aspects:

  • the minter is set to the address deploying the contract
  • all addresses have zero coins on deployment
  • minting works when the function is called from the minter address
  • minting does not work when the function is called from any other address
  • sending coins works

Here is the test file:

// import the contract artifact
const ExampleCoin = artifacts.require('./ExampleCoin.sol')

// test starts here
contract('ExampleCoin', function (accounts) {
  // predefine the contract instance
  let ExampleCoinInstance

  // before each test, create a new contract instance
  beforeEach(async function () {
    ExampleCoinInstance = await ExampleCoin.new()
  })

  // first test: define what it should do in the string
  it('should set account 0 to the minter', async function () {
    // minter is a public variable in the contract so you can get it directly via the created call function
    let minter = await ExampleCoinInstance.minter()
    // check whether minter is equal to account 0
    assert.equal(minter, accounts[0], "minter wasn't properly set")
  })
  // second test
  it('should initialize with 0 coins in account all accounts', async function () {
    // loop over the accounts
    for (let i = 0; i < 10; i++) {
      // get the balance of this account
      let balance = await ExampleCoinInstance.balances(accounts[i])
      // check that the balance is 0
      assert.equal(balance.toNumber(), 0, "0 wasn't in account" + i.toString())
    }
  })
  // third test
  it('should mint 10 coins if sent from account 0', async function () {
    // call the mint function to mint 10 coins to account 0
    await ExampleCoinInstance.mint(accounts[0], 10, { 'from': accounts[0] })
    // retrieve the updated balance of account 0
    let balance = await ExampleCoinInstance.balances(accounts[0])
    // check that the balance is now 10
    assert.equal(balance.toNumber(), 10, "10 wasn't in account 0")
  })
  // fourth test
  it('should send 5 coins from account 0 to account 1', async function () {
    // call the mint function to mint 10 coins to account 0
    await ExampleCoinInstance.mint(accounts[0], 10, { 'from': accounts[0] })
    // send 5 coins from account 0 to account 1
    await ExampleCoinInstance.send(accounts[1], 5, { 'from': accounts[0] })
    // retrieve the balances of account 0 and account 1
    let balance0 = await ExampleCoinInstance.balances(accounts[0])
    let balance1 = await ExampleCoinInstance.balances(accounts[1])
    // check that both balances are equal to 5
    assert.equal(balance0.toNumber(), 5, "5 wasn't in account 0")
    assert.equal(balance1.toNumber(), 5, "5 wasn't in account 1")
  })
  // more tests here
})

Of course, this test file is not complete. For example, you need to test the behavior of your contract when you send coins from an address that does not hold the necessary funds.

To run your test, open a shell, navigate to your project directory, and run

$ truffle test

Testing revert

If you remember, we required the minter of coins to be the owner of the contract. No other address than the one the deployed the contract should be able to mind coins. Otherwise, the mint functions should revert. Let's test for that.

Testing for revert is not straight forward but requires an extra package. Go ahead and install it in your project's directory via

$ npm install truffle-assertions

Next, in your test file ExampleCoin.js, import the newly install package by adding the following line at the top:

const truffleAssert = require('truffle-assertions')

Finally, add the following test where it says //more tests here:

  // fifth test
    it('should mint no coins if sent from account 1', async function () {
    // try to mint 10 coins from using account 1
    await truffleAssert.reverts(ExampleCoinInstance.mint(accounts[1], 10, { 'from': accounts[1] }))
    // fetch the balance of account 1
    let balance = await ExampleCoinInstance.balances(accounts[1])
    // check that the balance of account 1 is still 0
    assert.equal(balance.toNumber(), 0, "0 wasn't in account 1")
  })

Now run your test again and you should see the following output:

Using network 'test'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: ExampleCoin
    ✓ should set account 0 to the minter
    ✓ should initialize with 0 coins in account all accounts (198ms)
    ✓ should mint 10 coins if sent from account 0 (53ms)
    ✓ should send 5 coins from account 0 to account 1 (138ms)
    ✓ should mint no coins if sent from account 1 (58ms)


  5 passing (736ms)

Deploying to Ganache

Once you are certain that your contract behaves the way you intended, you can deploy it. First, we need to configure the truffle-config.js file. When you open it, you will see that it already contains a bunch of useful information, that is completely commented out. Take your time and read through the comments because they explain all the parts quite nicely.

Now, add your Ganache network in the networks object:

  networks: {
    // ...
    ganache: {
     host: "127.0.0.1",     // Localhost
     port: 7545,            // Standard Ganache port
     network_id: "*",       // Any network
    }                       // you may have to add a , here
    // ...
  }

For now, we will only deploy to Ganache. Therefore, the host is 127.0.0.1 or localhost. Different nodes and emulators listen to different ports by default:

  • Ganache: 7545
  • Truffle develop: 9545
  • Public and private node: whatever you specified in the genesis.json file

Make sure that Ganache is running. Then, in your terminal, run the deploy command:

$ truffle migrate

To redeploy the contract after altering it, run

$ truffle migrage --compile-all --reset
  • compile-all: forces a recompilation of all your solidity files
  • reset: resets the state of the migrations contract and foresees a rerun of all the migration scripts

You may also want to use a specific network (if you have multiple defined). This is done via

$ truffle migrate --network <NETWORK-NAME-IN-CONFIG>

The terminal will print the following output:

Compiling your contracts...
===========================
> Compiling ./contracts/ExampleCoin.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /home/sabine/git/teaching/ExampleCoin/build/contracts
> Compiled successfully using:
   - solc: 0.5.12+commit.7709ece9.Emscripten.clang



Starting migrations...
======================
> Network name:    'ganache'
> Network id:      5777
> Block gas limit: 0x4a817c800


1_initial_migration.js
======================

   Replacing 'Migrations'
   ----------------------
   > transaction hash:    0x266e3f0fe765a066c84f9999f81fa4696b73aa3b543857eb68f0b16bfb81e246
   > Blocks: 0            Seconds: 0
   > contract address:    0xAf7695fFa9CEa21d36a0A5a6db6913f529ACA9ED
   > block number:        1
   > block timestamp:     1579184865
   > account:             0xe0CA74697d8991352b0cb89AeA322D162898310F
   > balance:             99.99472518
   > gas used:            263741
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00527482 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00527482 ETH


2_deploy_contracts.js
=====================

   Replacing 'ExampleCoin'
   -----------------------
   > transaction hash:    0x426e98e3f5980daa89860abefc4fdc3ae6fbc7ce5c867271adeb779862318cc2
   > Blocks: 0            Seconds: 0
   > contract address:    0xc1980F745A5E00109d965d02e127b96bFA63c4BB
   > block number:        3
   > block timestamp:     1579184866
   > account:             0xe0CA74697d8991352b0cb89AeA322D162898310F
   > balance:             99.98478228
   > gas used:            455122
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00910244 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00910244 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.01437726 ETH

Once you deployed your contract, you should see four transactions on the first account on Ganache:

  • deployment of the Migrations.sol contract
  • call to the Migrations contract to update the last completed migration field of the Migrations contract
  • deployment of the ExampleCoin.sol contract
  • call to the Migrations contract to update the last completed migration field Migrations contract

Deploying to a test network

In order to deploy to a test network like Ropsten or Rinkeby, we need a number of things:

  1. We need an account on the test network
  2. We need funds on that account
  3. We need an Infura API key
  4. We have to add the test network to the truffle-config.js file

An account on the test network

You actually already have an account on the test network. When you set up your MetaMask, it created an Account 1. This account is linked to all networks via the mnemonic, which is the sequence of words MetaMask wanted you to write down. It is used to derive your private key. Hence, all you need to do is open up MetaMask and change the network to "Rinkeby" and Account 1 is automatically your Rinkeby account.

Add funds to the account

Ropsten and Rinkeby both have faucets that will give you some test ether. The Rinkeby faucet wants you to put your address on social media to avoid spamming. You then paste the link to the social media post into the faucet and they will send you ether. The Ropsten faucet is easier. There, you just paste your address. However, since the network is slower, it takes longer to receive the test ether.

Whichever faucet you use, you should receive some test either in Account 1 on the corresponding network.

Get an Infura API key

Infura allows you to access any network, test or main, via an API. The advantage of this is that you don't have to sync an entire chain to deploy a contract. The disadvantage is, that you have to trust the Infura API. Hence, if you are just testing things, feel free to use Infura. If you are deploying production code, it may be worth it syncing the main chain and deploying it directly.

The said, let's go ahead and create an Infura API key. You will have to create an account first. Once you have an account, you can create a project.

Create project

Give it any name. I called mine "Test". Next, view the project.

View project

Finally, retrieve your API key.

View project

Add the test network to the truffle-config.js

First, you will have to uncomment the following lines

// const HDWalletProvider = require('truffle-hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();

Add the Infura project ID as infuraKey:

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "8f348cd258db4921a1ebf############";

const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();

Next, create a file called .secret and paste your mnemonic words

fish ball exit run ...

Finally, add the network in the networks object. Here are the examples for Ropsten:

  ropsten: {
    provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/${infuraKey}`),
    network_id: 3, // Ropsten's id
    gas: 5500000 // Ropsten has a lower block limit than mainnet
  }

and Rinkeby:

  rinkeby: {
    provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${infuraKey}`),
    network_id: 4 // Rinkeby's id
  }

Deploy - Finally!

Switch back to the terminal and install the missing packages:

$ npm install truffle-hdwallet-provider fs

Now run the deploy command:

$ truffle deploy --network ropsten

(The command is equivalent for rinkeby.)