Unit Testing in Truffle

Hi @Bhujanga

Setup the network in your truffle config file.


     ganache: {
      host: "127.0.0.1",     // Localhost (default: none)
      port: 7545,            // Standard Ethereum port (default: none)
      network_id: "*",       // Any network (default: none)
     },

Make sure that the host and port are the ones indicated in Ganache RPC server:

Cheers,
Dani

1 Like

Thank you! Your reply gave me the final hint. So the problem was that I set the host and port right, but did not change the name of the network from development to ganache. So therefore I already connected to ganache when using truffle development command.
So for anyone encountering this problem, your truffle-config.js should look like this: (Note that I also updated to latest version of truffle by using:

npm uninstall -g truffle //run this first
npm install -g truffle //run this second
/**
 * Use this file to configure your truffle project. It's seeded with some
 * common settings for different networks and features like migrations,
 * compilation and testing. Uncomment the ones you need or modify
 * them to suit your project as necessary.
 *
 * More information about configuration can be found at:
 *
 * trufflesuite.com/docs/advanced/configuration
 *
 * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
 * to sign your transactions before they're sent to a remote public node. Infura accounts
 * are available for free at: infura.io/register.
 *
 * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
 * public/private key pairs. If you're publishing your code to GitHub make sure you load this
 * phrase from a file you've .gitignored so it doesn't accidentally become public.
 *
 */

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

module.exports = {
  /**
   * Networks define how you connect to your ethereum client and let you set the
   * defaults web3 uses to send transactions. If you don't specify one truffle
   * will spin up a development blockchain for you on port 9545 when you
   * run `develop` or `test`. You can ask a truffle command to use a specific
   * network from the command line, e.g
   *
   * $ truffle test --network <network-name>
   */

  networks: {
   

    **ganache: {**
**    host: "127.0.0.1",     // Localhost (default: none)**
**    port: 7545,            // Standard Ethereum port (default: none)**
**    network_id: "5777",       // Any network (default: none)**
**    },**
    // Another network with more advanced options...
    // advanced: {
    // port: 8777,             // Custom port
    // network_id: 1342,       // Custom network
    // gas: 8500000,           // Gas sent with each transaction (default: ~6700000)
    // gasPrice: 20000000000,  // 20 gwei (in wei) (default: 100 gwei)
    // from: <address>,        // Account to send txs from (default: accounts[0])
    // websocket: true        // Enable EventEmitter interface for web3 (default: false)
    // },
    // Useful for deploying to a public network.
    // NB: It's important to wrap the provider as a function.
    // ropsten: {
    // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
    // network_id: 3,       // Ropsten's id
    // gas: 5500000,        // Ropsten has a lower block limit than mainnet
    // confirmations: 2,    // # of confs to wait between deployments. (default: 0)
    // timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
    // skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
    // },
    // Useful for private networks
    // private: {
    // provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
    // network_id: 2111,   // This network is yours, in the cloud.
    // production: true    // Treats this network as if it was a public net. (default: false)
    // }
  },

  // Set default mocha options here, use special reporters etc.
  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      // version: "0.5.1",    // Fetch exact version from solc-bin (default: truffle's version)
      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
      // settings: {          // See the solidity docs for advice about optimization and evmVersion
      //  optimizer: {
      //    enabled: false,
      //    runs: 200
      //  },
      //  evmVersion: "byzantium"
      // }
    }
  }
};

EDIT: Ok I get it. with truffleAssert.fails we check if error is catched. With truffleAssert.passes we check that functionality is given!

So, the first test should pass, at it should not be allowed to delete person by not owner. and the second test should fail (AssertionError: Did not fail) as the contract should allow the contract owner to delete a person. Am I getting this right?

  it("should not allow to delete person by not owner", async function(){
    let instance = await Peoples.deployed();
    await instance.createPerson("SomeoneNew", 14, 165, {value: web3.utils.toWei("1", "ether"), from: accounts[0]});
    await truffleAssert.fails(instance.deletePerson(accounts[0], {from: accounts[1]}));
  })
 it("should allow to delete person created by owner of person", async function(){
   let instance = await Peoples.deployed();
   await instance.createPerson("SomeoneNew", 14, 165, {value: web3.utils.toWei("1", "ether"), from: accounts[0]});
   await truffleAssert.fails(instance.deletePerson(accounts[1], {from: accounts[0]}));
 })

Can you post your contract?

which contract? peoples?

import "./Ownable.sol";
pragma solidity 0.5.16;

contract Peoples is Ownable{

    struct Person {
      uint id;
      string name;
      uint age;
      uint height;
      bool senior;
    }

    event personCreated(string name, bool senior);
    event personDeleted(string name, bool senior, address deletedBy);

    uint public balance;

    modifier costs(uint cost){
        require(msg.value >= cost);
        _;
    }

    mapping (address => Person) private people;
    address[] private creators;

    function createPerson(string memory name, uint age, uint height) public payable costs(1 ether){
      require(age < 150, "Age needs to be below 150");
      require(msg.value >= 1 ether);
      balance += msg.value;

        //This creates a person
        Person memory newPerson;
        newPerson.name = name;
        newPerson.age = age;
        newPerson.height = height;

        if(age >= 65){
           newPerson.senior = true;
       }
       else{
           newPerson.senior = false;
       }

        insertPerson(newPerson);
        creators.push(msg.sender);

        assert(
            keccak256(
                abi.encodePacked(
                    people[msg.sender].name,
                    people[msg.sender].age,
                    people[msg.sender].height,
                    people[msg.sender].senior
                )
            )
            ==
            keccak256(
                abi.encodePacked(
                    newPerson.name,
                    newPerson.age,
                    newPerson.height,
                    newPerson.senior
                )
            )
        );
        emit personCreated(newPerson.name, newPerson.senior);
    }
    function insertPerson(Person memory newPerson) private {
        address creator = msg.sender;
        people[creator] = newPerson;
    }
    function getPerson() public view returns(string memory name, uint age, uint height, bool senior){
        address creator = msg.sender;
        return (people[creator].name, people[creator].age, people[creator].height, people[creator].senior);
    }
    function deletePerson(address creator) public onlyOwner {
      string memory name = people[creator].name;
      bool senior = people[creator].senior;

       delete people[creator];
       assert(people[creator].age == 0);
       emit personDeleted(name, senior, owner);
   }
   function getCreator(uint index) public view onlyOwner returns(address){
       return creators[index];
   }
   function withdrawAll() public onlyOwner returns(uint) {
       uint toTransfer = balance;
       balance = 0;
       msg.sender.transfer(toTransfer);
       return toTransfer;
   }

}

it("should increase balance of contract after a person is created", async function(){
   let instance = await Peoples.deployed();
   let oldBalance = await web3.eth.getBalance(Peoples.address);
   await instance.createPerson("SomeoneNew", 14, 165, {value: web3.utils.toWei("1", "ether"), from: accounts[0]});
   let newBalance = await web3.eth.getBalance(Peoples.address);
   assert(newBalance == parseInt(oldBalance) + parseInt(web3.utils.toWei("1", "ether")), "Balanced not updated");
 });
 it("should allow owner to withdraw contract balance", async function(){
   let instance = await Peoples.deployed();
   await instance.withdrawAll();
   truffleAssert.passes(instance.withdrawAll({from: accounts[0]}));
 });
 it("should increase owner balance after withdrawal", async function(){

   let instance = await Peoples.new();
   await instance.createPerson("AnotherOne", 22, 212, {from: accounts[2], value: web3.utils.toWei("1", "ether")});
   let BalBefWitDrw = parseFloat(await web3.eth.getBalance(accounts[0]));
   await instance.withdrawAll();
   let BalAftWitDrw = parseFloat(await web3.eth.getBalance(accounts[0]));
   assert(BalBefWitDrw < BalAftWitDrw, "Owners balance was not updated");

 });
 it("should not allow non-owner to withdraw", async function(){
   let instance = await Peoples.deployed();
   await instance.createPerson("ANewPerson", 34, 189, {value: web3.utils.toWei("1", "ether"), from: accounts[2]});
   truffleAssert.fails(instance.withdrawAll({from: accounts[1]}));
 });
 it("should update balance of contract to 0 after withdrawal", async function(){
   let instance = await Peoples.new();
   await instance.createPerson("ANewPerson", 34, 189, {value: web3.utils.toWei("1", "ether"), from: accounts[1]});
   await instance.withdrawAll();
   let instanceBalance = await instance.balance();
   let floatBalance = parseFloat(instanceBalance);
   let chainBalance = await web3.eth.getBalance(instance.address);
   assert(floatBalance == web3.utils.toWei("0", "ether") &&  floatBalance == chainBalance, "Contract balance not reset after withdrawal");
 });
const truffleAssert = require("truffle-assertions");
const People = artifacts.require("People");

contract("People", async accounts => {
    let [ owner, nonOwner ] = accounts;
    
    it("should delete a person when called by owner", async () => {
        let people = await People.deployed();
        await people.createPerson("Paddy", "19", "172", {from: owner, value: web3.utils.toWei("1", "ether")});
        let receipt = await people.deletePerson(owner, {from: owner});

        assert(receipt.logs[0].args.name === "Paddy");
    });

    it("should not allow non-owner to delete person", async () => {
        let people = await People.deployed();
        await people.createPerson("John", "30", "180", {from: nonOwner, value: web3.utils.toWei("1", "ether")});
        
        await truffleAssert.fails(people.deletePerson(nonOwner, {from: nonOwner}));
    })
});

Hey @Bhujanga

In your contract, the function deletePerson() is restricted to the owner:

 function deletePerson(address creator) public onlyOwner {}

In your test, it seems like you are calling that function using the owner account therefore it should not fail.
Why are you using truffleAssert.fails?

await truffleAssert.fails(instance.deletePerson(accounts[1], {from: accounts[0]}));

Cheers,
Dani

I see! thank you! In this case, if I want to test to “Fail” I would test it by trying to delete from another account than owner account. Also, if I use truffleAssert.passes instead, I can create a test that would for example say it(“should allow owner to delete Person”)

1 Like

Testing the ownerOnly modifier

const truffleAssert = require("truffle-assertions");
const web3 = require("web3");
const People = artifacts.require("People");


contract("People", async (accounts) => {
    it ("Person should not be deleted others than the owner", async function () {
        let instance = await People.deployed();
        await instance.createPerson("Heidi", 20, 190, {value: web3.utils.toWei("1", "ether"), from: accounts[0]});
        await truffleAssert.fails(instance.deletePerson(accounts[0], {from: accounts[1]}), truffleAssert.ErrorType.REVERT);
    })
    it ("Person should be deleted by the owner", async function () {
        let instance = await People.deployed();
        await instance.createPerson("Heidi", 20, 190, {value: web3.utils.toWei("1", "ether"), from: accounts[0]});
        await truffleAssert.passes(instance.deletePerson(accounts[0], {from: accounts[0]}));
    })
});
1 Like

Testing the withdraw function
The tests work, however when I use Contract.new() instead of Contract.deployed() it doesn’t work. Here’s my solution:

const People = artifacts.require("People");

contract("People", async accounts => {
    let people, peopleAddress, bal, contractBal;
    const [ owner, nonOwner ] = accounts;

    beforeEach("set up contract", async () => {
        people = await People.deployed();
        peopleAddress = People.address;
    });

    it("should receive correct Ether when createPerson is called", async () => {
        await people.createPerson("Paddy", "19", "172", {from: owner, value: web3.utils.toWei("1", "ether")});
        bal = await web3.eth.getBalance(peopleAddress);
        bal = await web3.utils.fromWei(bal);

        assert.equal(bal, 1);
    });

    it("contract balance variable should be same as contract Ether balance", async () => {
        await people.createPerson("Paddy", "19", "172", {from: owner, value: web3.utils.toWei("1", "ether")});

        bal = await web3.eth.getBalance(peopleAddress);
        bal = web3.utils.fromWei(bal);
        contractBal = await people.balance();
        contractBal = web3.utils.fromWei(contractBal);

        assert.equal(contractBal, bal);
    });

    it("should allow owner to withdraw all Ether value and deduct balance", async () => {
        await people.createPerson("Bob", "40", "180", {from: nonOwner, value: web3.utils.toWei("1", "ether")});
        bal = web3.utils.fromWei(await people.balance());

        assert.notEqual(bal, 0);

        await people.withdrawAll({from: owner});
        bal = web3.utils.fromWei(await people.balance());

        assert.equal(bal, 0);
    });
});

Hey @paddyc1

Can you change people = await People.deployed(); to people = await People.new(); and post the results of your tests?

Thanks,
Dani

It seems to read balance as 0 when I use People.new() :

Contract: People
    1) should receive correct Ether when createPerson is called

    Events emitted during test:
    ---------------------------

    People.personCreated(
      name: 'Paddy' (type: string),
      senior: false (type: bool)
    )


    ---------------------------
    2) contract balance variable should be same as contract Ether balance

    Events emitted during test:
    ---------------------------

    People.personCreated(
      name: 'Paddy' (type: string),
      senior: false (type: bool)
    )


    ---------------------------
    ✓ should allow owner to withdraw all Ether value and deduct balance (278ms)


  1 passing (1s)
  2 failing

  1) Contract: People
       should receive correct Ether when createPerson is called:
     AssertionError: expected '0' to equal 1
      at Context.<anonymous> (test/People.js:18:16)
      at processTicksAndRejections (internal/process/task_queues.js:97:5)

  2) Contract: People
       contract balance variable should be same as contract Ether balance:

      AssertionError: expected '1' to equal '0'
      + expected - actual

      -1
      +0
      
      at Context.<anonymous> (test/People.js:29:16)
      at processTicksAndRejections (internal/process/task_queues.js:97:5)

Hey @paddyc1

Keep in mind that, if you declare people.new inside the before each function, a new contract instance will be used for each test.
The modifications done in the 1st test will not reflect in the 2nd test.
Do you need beforeEach? I think before() will be better here.

Cheers,
Dani

1 Like

testing onlyOwnerCase

async function (){
        let instance = await People.deployed()
        await instance.createPerson("Bob", 65, 190, {from: accounts[2],value: web3.utils.toWei("1", "Ether")})
        await truffleAssert.fails(instance.deletePerson(accounts[2], {from: accounts[5]}), truffleAssert.ErrorType.REVERT)
    }

Ah OK, not sure how I didn’t notice that. Thanks for the tip.

1 Like

withdraw tests

contract("People", async function (accounts){
    it("should increase contract balance when create person", async function () {
        let instance = await People.new()
        await instance.createPerson("Bob", 65, 190, {from: accounts[2],value: web3.utils.toWei("1", "Ether")})
        let contractBalance =  web3.utils.fromWei(await web3.eth.getBalance(instance.address))
        let contractBalanceVariable = web3.utils.fromWei(await instance.balance())

        assert(contractBalance === "1", "Incorrect blockchain contract balance after person creation")
        assert(contractBalanceVariable === "1", "Incorrect balance variable of the contract after person creation")
    })
    it("owner balance should change after withdrawAll", async function () {
        let instance = await People.new()
        await instance.createPerson("Bob", 65, 190, {from: accounts[8],value: web3.utils.toWei("1", "Ether")})

        let ownerBalanceBefore = web3.utils.fromWei(await web3.eth.getBalance(accounts[0]))
        await instance.withdrawAll({from: accounts[0]})
        let ownerBalanceAfter = web3.utils.fromWei(await web3.eth.getBalance(accounts[0]))
        assert(ownerBalanceBefore < ownerBalanceAfter, "Balance has not been changed after withdraw")
    })
    it("contract balance should change after withdrawAll", async function () {
        let instance = await People.new()
        await instance.createPerson("Bob", 65, 190, {from: accounts[8],value: web3.utils.toWei("1", "Ether")})

        let contractBalanceBefore = web3.utils.fromWei(await web3.eth.getBalance(instance.address))
        let contractBalanceVariableBefore = web3.utils.fromWei(await instance.balance())
        await instance.withdrawAll({from: accounts[0]})
        let contractBalanceAfter = web3.utils.fromWei(await web3.eth.getBalance(instance.address))
        let contractBalanceVariableAfter = web3.utils.fromWei(await instance.balance())

        assert(contractBalanceBefore === "1", "Incorrect contract balance before withdraw")
        assert(contractBalanceAfter === "0", "Incorrect contract balance after withdraw")
        assert(contractBalanceVariableBefore === "1", "Incorrect balance variable before withdraw")
        assert(contractBalanceVariableAfter === "0", "Incorrect balance variable after withdraw")
    })

})
1 Like

No probs :slight_smile: happy learning

My truffleAssert.fails() tests are working fine, but I’m having a bit of an issue with the assert tests testing the senior boolean and the age. It also edited the personCreated event in order to check the both the age and senior values and it says both are correct there, so I’m not sure why my assert functions fail saying senior is not set to true and age is not set correctly?

For the last test, when I use toNumber() like in the video I get a TypeError, but when I remove the toNumber() the assert simply fails. Why is this?

Is there something wrong with the getPerson() function such that result is not actually storing the created person into result?

.sol code

pragma solidity 0.5.12;
//pragma experimental ABIEncoderV2;
import "./Ownable.sol";

contract People is Ownable {

    struct Person {
        string name;
        uint age;
        uint height;
        bool senior;
    }

    event personCreated(string name, uint age, uint height, bool senior);
    event personDeleted(string name, bool senior, address deletedBy);

    uint public balance;

    modifier costs(uint cost) {
        require(msg.value >= cost);
        _;
    }

    mapping (address => Person) private people;
    address[] private creators;

    //costs 1 ether to run
    function createPerson(string memory _name, uint _age, uint _height) public payable costs(1 ether) {
        require(_age < 150, "TOO OLD!");
        //require(msg.value >= 1 ether);    //requires 1 wei to execute
        //balance += msg.value;       //add to balance value that was sent to this function/contract

        Person memory newPerson;
        newPerson.name = _name;
        newPerson.age = _age;
        newPerson.height = _height;

        if(_age >= 65) {
            newPerson.senior = true;
        } else {
            newPerson.senior = false;
        }

        insertPerson(newPerson);
        creators.push(msg.sender);

        //assert using to == compare hash of both using keccak256 hash function
        //essentially checking if people[msg.sender] == newPerson
        assert(
            keccak256(
                abi.encodePacked(
                    people[msg.sender].name,
                    people[msg.sender].age,
                    people[msg.sender].height,
                    people[msg.sender].senior
                )
            )
            ==
            keccak256(
                abi.encodePacked(
                    newPerson.name,
                    newPerson.age,
                    newPerson.height,
                    newPerson.senior
                )
            )
        );
        emit personCreated(newPerson.name, newPerson.age, newPerson.height, newPerson.senior);
    }

    function insertPerson(Person memory _person) private {
        address creator = msg.sender;
        people[creator] = _person;
    }

    /*for use with ABIEncoderV2
    function getPerson() public view returns (Person memory) {
        return people[msg.sender];
    }*/

    function getPerson() public view returns (string memory, uint, uint, bool) {
        address creator = msg.sender;
        return (people[creator].name, people[creator].age, people[creator].height, people[creator].senior);
    }

    function deletePerson(address creator) public onlyOwner {
        string memory name = people[creator].name;
        bool senior = people[creator].senior;

        delete people[creator];
        assert(people[creator].age == 0);
        emit personDeleted(name, senior, msg.sender);
    }

    function getCreator(uint index) public view onlyOwner returns (address) {
        return creators[index];
    }

    function withdrawAll() public onlyOwner returns (uint) {
        uint toTransfer = balance;      //make temp var to send, reset balance to 0, done for security reasons
        balance = 0;
        msg.sender.transfer(toTransfer);    //transfer can only be used by an address, transfers to address, which in this case
                                            //is the owner, or msg.sender if already here. Reverts if transfer fails
        /*
        msg.sender.send(toTransfer);        //this is the same as transfer but returns false if fails instead of reverts.
                                            //hence, this requires manual reverting or else funds are lost.
        if(msg.sender.send(toTransfer)) {
            return toTransfer;
        } else {
            balance = toTransfer;
            return 0;
        }
        */
        return toTransfer;
    }
}

test.js code

const People = artifacts.require("People");
const truffleAssert = require("truffle-assertions");


contract("People", async function(){
  //test and assert contract shouldn't make person with age over 150
  it("shouldn't make person with age over 150", async function() {
    let instance = await People.deployed();
    //use assert to test failure, asserting that createPerson fails if age 200 since it's >150
    await truffleAssert.fails(
      instance.createPerson("Bob", 200, 300, {value: web3.utils.toWei("1", "ether")}),
      truffleAssert.ErrorType.REVERT  //test passes upon REVERT, we WANT to assert contract REVERT if age >150
    );
  })

  //test and assert contract shouldn't create person without valid payment amount
  it("shouldn't create a person without payment", async function(){
    let instance = await People.deployed();

    await truffleAssert.fails(
      instance.createPerson("Bob", 50, 300, {value: 1000}),
      truffleAssert.ErrorType.REVERT  //test passes upon REVERT, we WANT to assert contract REVERT if age >150
    );
  })

  //test and assert contract should set senior flag correctly
  it("should set senior status correctly", async function() {
    //create person with age >65
    let instance = await People.deployed();
    await instance.createPerson("Bob", 65, 300, {value: web3.utils.toWei("1", "ether")});

    let result = await instance.getPerson();
    assert(result.senior === true, "should be senior!");
  })

  it("should set age correctly", async function() {
    let instance = await People.deployed();
    let result = await instance.getPerson();
    assert(result.age.toNumber() === 65, "Age not set correctly!");
  })
});

Terminal output:

Thanks for all the help!

I tried testing my code in remix as well and it says the senior boolean is true so I think the issue is either the getPerson() or the assert?