Unit Testing with Chai – How to Simulate or Force a Failed Transaction

chaitransactionsunittesting

Using hardhat and chai, is there a way to simulate/force a transaction failure so the error code will execute?

e.g. with the following, I want it to throw the error because the transaction didn't succeed.

function withdraw() public onlyOwner {
    uint amount = address(this).balance;
 
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Failed to withdraw contract balance");
}

Can I do that with chai or another way?

To clarify, the point is to create a test that will execute this line of code

require(success, "Failed to withdraw contract balance");

with a status of success == false (the transaction failed bit) so the unit tests cover that branch of code, and without altering the code of the contract being tested.

I don't want to do something like put in !success or otherwise change the code of the contract being tested. I think that would be bad practice so would like to find another way.

Best Answer

minimal code to implement Lauri's suggestion

Target.sol (contract being tested)

pragma solidity ^0.8.10;

contract Target {

    address payable owner;

    constructor () {
        owner = payable(msg.sender);
    }

    modifier onlyOwner {
        require(msg.sender == owner, "only owner");
        _;
    }

    function withdraw() external onlyOwner{
        uint _balance = address(this).balance;
        (bool success,) = owner.call{value: _balance}("");
        require(success, "Withdraw transaction failed");
    }
}

TestHelper.sol can't receive value as it has neither receive nor fallback functions

pragma solidity ^0.8.10;

import "./Target.sol";

contract TestHelper {

    Target targetContract;

    constructor () {
        targetContract = new Target();
    }

    function targetWithdraw() external {
        targetContract.withdraw();
    }
}

target.test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Test target contract withdraw", function () {
    it("reverts as expected on failed transaction", async function () {
        const testHelperFactory = await ethers.getContractFactory('TestHelper')
        testHelper = await testHelperFactory.deploy()
        await testHelper.deployed()
        await expect(testHelper.targetWithdraw()).to.be.revertedWith("Withdraw transaction failed")
    })
});
Related Topic