Lesson 15 of 21
In Progress

Advanced Testing and Debugging Techniques

As a blockchain developer, it’s essential to test and debug your contracts to ensure they are functioning correctly. In this article, we’ll cover advanced testing and debugging techniques that you can use to improve the reliability of your contracts.

Using Mocks and Stubs in Tests

Mocks and stubs are test doubles that allow you to simulate the behavior of external dependencies in your tests. Mocks and stubs can be useful when you want to isolate your tests from external dependencies or when you want to test error cases.

To use mocks and stubs in your tests, you can use a mocking library such as @machinomy/solidity-test-utils. This library provides a set of functions that you can use to create mocks and stubs for your contracts.

For example, to create a mock for the MyContract contract, you can use the following code:

import { solidityTestUtils } from "@machinomy/solidity-test-utils";

const MyContract = artifacts.require("MyContract");
const MyContractMock = solidityTestUtils.mock(MyContract);

To create a stub for the MyContract contract, you can use the following code:

import { solidityTestUtils } from "@machinomy/solidity-test-utils";

const MyContract = artifacts.require("MyContract");
const MyContractStub = solidityTestUtils.stub(MyContract);

Using Contract Fuzzing

Contract fuzzing is a technique for testing contracts by generating random input data and executing the contract with the generated data. Contract fuzzing can help you find edge cases and bugs in your contracts that you may not have thought of.

To use contract fuzzing, you can use a fuzzing library such as eth-contract-fuzzer. This library provides a set of functions that you can use to generate random input data and execute your contracts with the generated data.

For example, to use contract fuzzing with the MyContract contract, you can use the following code:

import { contractFuzzer } from "eth-contract-fuzzer";

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

contractFuzzer.fuzz(MyContract);

Using Debugging Tools

Debugging tools can be useful when you need to inspect the state of your contracts or trace the execution of your contracts. Some popular debugging tools for Ethereum contracts include Remix, Truffle Debug, and Hardhat Debug.

Remix

Remix is an online compiler and debugger for Solidity contracts. You can use Remix to write, compile, and debug your contracts in the browser. Remix also provides a built-in debugger that allows you to step through the execution of your contracts and inspect the state of your contracts.

To use Remix, you can visit the Remix website and follow the instructions in the user interface.

Truffle Debug

Truffle Debug is a debugging tool for Truffle projects. You can use Truffle Debug to debug your contracts by attaching a debugger to a running Truffle test or by running Truffle’s built-in debugger.

To use Truffle Debug, you’ll need to install the truffle-debugger package and configure your Truffle project to use it.

First, install the truffle-debugger package by running the following command:

npm install truffle-debugger

Next, add the truffle-debugger plugin to your truffle-config.js file:

module.exports = {
  // ...
  plugins: ["truffle-debugger"],
  // ...
};

To attach a debugger to a running Truffle test, you can use the debug command and specify the test file and line number that you want to debug:

truffle debug test/my-tests.js:12

To run Truffle’s built-in debugger, you can use the debug command and specify the contract and function that you want to debug:

truffle debug MyContract.myFunction

Hardhat Debug

Hardhat Debug is a debugging tool for Hardhat projects. You can use Hardhat Debug to debug your contracts by attaching a debugger to a running Hardhat task or by running Hardhat’s built-in debugger.

To use Hardhat Debug, you’ll need to install the @hardhat/debug plugin and configure your Hardhat project to use it.

First, install the @hardhat/debug plugin by running the following command:

npm install @hardhat/debug

Next, add the @hardhat/debug plugin to your hardhat.config.js file:

module.exports = {
  // ...
  plugins: ["@hardhat/debug"],
  // ...
};

To attach a debugger to a running Hardhat task, you can use the debug command and specify the task that you want to debug:

hardhat debug compile

To run Hardhat’s built-in debugger, you can use the debug command and specify the contract and function that you want to debug:

hardhat debug MyContract.myFunction

Conclusion

In this article, we’ve covered advanced testing and debugging techniques that you can use to improve the reliability of your contracts. By using mocks and stubs in your tests, contract fuzzing, and debugging tools such as Remix, Truffle Debug, and Hardhat Debug, you can catch and fix bugs in your contracts more efficiently.

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

In this exercise, you’ll use a mock to isolate your test from an external dependency.
-Create a contract MyContract that depends on an external library MyLibrary.
-Write a test for MyContract that calls a function that uses MyLibrary.
-Use a mock to replace MyLibrary in the test.
-Assert that the function call to MyLibrary was made with the correct arguments.

pragma solidity ^0.6.0;

library MyLibrary {
  function add(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
  }
}

contract MyContract {
  using MyLibrary for *;

  function test(uint256 a, uint256 b) public view returns (uint256) {
    return MyLibrary.add(a, b);
  }
}
import { solidityTestUtils } from "@machinomy/solidity-test-utils";

const MyContract = artifacts.require("MyContract");
const MyLibraryMock = solidityTestUtils.mock(MyLibrary);

contract("MyContract", () => {
  it("should call MyLibrary.add with the correct arguments", async () => {
    const myContract = await MyContract.new();

    MyLibraryMock.expect(
      "add",
      (a, b) => {
        assert.equal(a, 1);
        assert.equal(b, 2);
      },
      { returns: 3 }
    );

    const result = await myContract.test(1, 2);
    assert.equal(result, 3);

    MyLibraryMock.verify();
  });
});

In this exercise, you’ll use contract fuzzing to find bugs in a contract.
-Create a contract MyContract that has a function test that takes a uint256 argument and returns it.
-Write a test that uses the fuzz function from solidity-test-utils to test the test function with a large number of random inputs.
-Run the test and observe any errors or failures.
-Debug and fix any bugs that are found.

pragma solidity ^0.6.0;

contract MyContract {
  function test(uint256 x) public view returns (uint256) {
    return x;
  }
}
import { solidityTestUtils } from "@machinomy/solidity-test-utils";

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

contract("MyContract", () => {
  it("should handle random inputs correctly", async () => {
    const myContract = await MyContract.new();

    await solidityTestUtils.fuzz(myContract.test);
  });
});

Write a test for a function called calculateTotal that takes in an array of numbers and returns their sum. The function should throw an error if the array is empty.

pragma solidity ^0.6.0;

contract Calculator {
    function calculateTotal(uint[] memory _numbers) public view returns (uint) {
        require(_numbers.length > 0, "Error: array cannot be empty");
        uint total = 0;
        for (uint i = 0; i < _numbers.length; i++) {
            total += _numbers[i];
        }
        return total;
    }
}

contract CalculatorTest {
    Calculator calculator = new Calculator();

    function testCalculateTotal() public {
        uint[] memory numbers = new uint[](3);
        numbers[0] = 1;
        numbers[1] = 2;
        numbers[2] = 3;
        assert.equal(calculator.calculateTotal(numbers), 6, "Error: total is incorrect");
    }

    function testCalculateTotalError() public {
        uint[] memory numbers = new uint[](0);
        try {
            calculator.calculateTotal(numbers);
            assert(false, "Error: expected throw but got none");
        } catch (error) {
            assert(error.message == "Error: array cannot be empty", "Error: incorrect error message");
        }
    }
}

Write a test for a function called transfer that takes in a recipient address and an amount, and transfers the specified amount from the contract’s balance to the recipient. The function should throw an error if the amount is greater than the contract’s balance.

pragma solidity ^0.6.0;

contract Wallet {
    address payable public owner;
    uint public balance;

    constructor() public {
        owner = msg.sender;
        balance = 100;
    }

    function transfer(address payable _recipient, uint _amount) public {
        require(_amount <= balance, "Error: insufficient balance");
        _recipient.transfer(_amount);
        balance -= _amount;
    }
}

contract WalletTest {
    Wallet wallet = new Wallet();

    function testTransfer() public {
        address recipient = address(0x1234567890abcdef1234567890abcdef12345678);
        wallet.transfer(recipient, 50);
        assert.equal(wallet.balance, 50, "Error: balance is incorrect");
        assert.equal(recipient.balance, 50, "Error: recipient balance is incorrect");
    }

    function testTransferError() public {
        address recipient = address(0x1234567890abcdef1234567890abcdef12345678);
        try {
            wallet.transfer(recipient, 150);
            assert(false, "Error: expected throw but got none");
        } catch (error) {
            assert(error.message == "Error: insufficient balance", "Error: incorrect error message");
        }
    }
}

Write a test for a Solidity function that takes in two uint256 variables, x and y, and returns the product of the two variables. The test should check that the function returns the correct result when called with different combinations of x and y.

pragma solidity ^0.7.0;

contract Multiply {
    function multiply(uint256 x, uint256 y) public pure returns (uint256) {
        return x * y;
    }
}

// Test for Multiply contract
contract MultiplyTest {
    // Create an instance of the Multiply contract
    Multiply multiply = new Multiply();

    // Test that the multiply function returns the correct result
    function testMultiply() public {
        // Test with x = 3 and y = 4
        assert(multiply.multiply(3, 4) == 12, "Multiply test failed: expected 12");

        // Test with x = 2 and y = 7
        assert(multiply.multiply(2, 7) == 14, "Multiply test failed: expected 14");

        // Test with x = 5 and y = 5
        assert(multiply.multiply(5, 5) == 25, "Multiply test failed: expected 25");
    }
}