I would like to write maintainable and readable unit tests for my code. Requirements:

  1. Test different actors. E.g. a bank contract and three customers interacting with it.
  2. Test for errors. E.g. test that require() or assert() in the contract work.
  3. Write many/long tests without running into Gas/memory problems.

Question: Can someone show me how to do this properly using built-in features of a unit testing framework? E.g. in remix-tests, truffle-test or any other framework.

This is an example of testing code I'd like to write:

contract BankTests {
    function test() public {
        Bank bank = new Bank(); // a bank that only allows two customers
        TestActor customer1 = new TestActor{value: 1 ether}(); // has 1 ether
        TestActor customer2 = new TestActor{value: 1 ether}(); // has 1 ether
        TestActor customer3 = new TestActor{value: 1 ether}(); // has 1 ether

        switchToActor(bank, customer1); // from now on customer1 interacts with the bank
        Assert.equal(bank.balance(), 0);
        bank.deposit{value: 0.5 ether}();
        Assert.equal(bank.balance(), 0.5 ether);
        bank.deposit{value: 0.25 ether}();
        Assert.equal(bank.balance(), 0.75 ether);

        switchToActor(bank, customer2); // from now on customer2 interacts with the bank
        Assert.equal(bank.balance(), 0);
        bank.deposit{value: 0.1 ether}();
        Assert.equal(bank.balance(), 0.1 ether);

        switchToActor(bank, customer3); // from now on customer3 interacts with the bank
        try bank.deposit{value: 0.5 ether}() {
            Assert.equal(true, false); // must not succeed; bank only allows 2 customers
        } catch Error(string memory reason) {
            Assert.equal(reason, "Bank already has two customers.");
        } catch {
            Assert.equal(true, false); // wrong error; we expect a reason

        resetActor(); // no current actor anymore
        Assert.equal(address(customer1).balance, 0.25 ether);
        Assert.equal(address(customer2).balance, 0.9 ether);
        Assert.equal(address(customer3).balance, 1 ether);

I didn't manage to achieve my three requirements with remix-tests and truffle-test with their built-in features. So I wrote my own framework that runs in remix-tests and truffle-test. This is a lot of code that I'd be happy to delete again if there is a better way to do this. Here a complete example of my approach:

// The contract we want to test (made up for this question):
contract Bank {
    mapping(address => uint256) internal customers;
    uint8 internal customerCount;

    function deposit() public payable {
        require(msg.value > 0, "No money provided.");
        require(customers[msg.sender] > 0 || customerCount < 2,
                "Bank already has two customers.");
        customers[msg.sender] += msg.value;
        if(customers[msg.sender] == msg.value) {

    function balance() public view returns (uint256) {
        return customers[msg.sender];

// One unit test:
// initialBalance and #value to get 3 ether from remix-tests or truffle-test.
contract BankTests is TestContract {
    uint64 public initialBalance = 3 ether;

    /// #value: 3000000000000000000
    function test() public payable {
        TestBank bank = new TestBank();
        TestActor customer1 = new TestActor{value: 1 ether}();
        TestActor customer2 = new TestActor{value: 1 ether}();
        TestActor customer3 = new TestActor{value: 1 ether}();

        switchToActor(bank, customer1);
        Assert.equal(bank.balance(), 0, "1a failed");
        bank.deposit{value: 0.5 ether}();
        Assert.equal(bank.balance(), 0.5 ether, "1b failed");
        bank.deposit{value: 0.25 ether}();
        Assert.equal(bank.balance(), 0.75 ether, "1c failed");

        switchToActor(bank, customer2);
        Assert.equal(bank.balance(), 0, "2a failed");
        bank.deposit{value: 0.1 ether}();
        Assert.equal(bank.balance(), 0.1 ether, "2b failed");

        switchToActor(bank, customer3);
        try bank.deposit{value: 0.5 ether}() {
            Assert.equal(true, false, "3a failed");
        } catch Error(string memory reason) {
                "Bank already has two customers.",
                "3b failed"
        } catch {
            Assert.equal(true, false, "3c failed");

        Assert.equal(address(customer1).balance, 0.25 ether, "4a failed");
        Assert.equal(address(customer2).balance, 0.9 ether, "4b failed");
        Assert.equal(address(customer3).balance, 1 ether, "4c failed");

// Base class of test contracts to enable actor switching:
contract TestContract {
    function switchToActor(TestBank bank, TestActor actor) internal {
        bank.switchToActor{value: address(this).balance}(actor);

    function resetActor(TestBank bank) internal {
        bank.resetActor{value: address(this).balance}();

    receive() external payable {} // accept payments

// An actor contract to hold money and interact with other contracts:
contract TestActor {
    constructor() payable {}

    function sendAllMoneyTo(address payable recipient) public {

    receive() external payable {} // accept payments

// Subclass of bank contract to enable actor switching:
contract TestBank is Bank {
    address payable private currentActualCustomerAddr;

    function switchToActor(TestActor actualCustomer) public payable {
        address payable proxyCustomerAddr = payable(msg.sender);
        address payable actualCustomerAddr = payable(address(actualCustomer));
        customers[proxyCustomerAddr] = customers[actualCustomerAddr];
        currentActualCustomerAddr = actualCustomerAddr;

    function resetActor() public payable {
        address payable proxyCustomerAddr = payable(msg.sender);
        if(currentActualCustomerAddr != address(0)) {
            customers[currentActualCustomerAddr] = customers[proxyCustomerAddr];
        currentActualCustomerAddr = payable(address(0));

The test contract (BankTests) is always the customer actually interacting with the bank contract. When requesting to switch to another actor, the test contract gets the balance of that actor and the bank contract data is modified so that the test contract gets the actors state. This is not very beautiful but allows - IMHO - to write readable and maintainable tests.

Writing multiple tests this way, one pretty soon runs into "gas limit exceeded" or "out of memory" errors:

  • To "solve" the out of memory error, I wrote my own test runner that runs one test contract at a time with remix-tests (remix-tests would load all test contracts/files together).
  • To "solve" the gas limit exceeded error, I changed the JavaScript source of remix-tests to have a 20 times higher gas limit (ugly hack). I didn't find a way to achieve the same for truffle-test.

To summarize: I built a workaround that I wish wasn't necessary. I hope someone shows me a nicer way to achieve the same. If this is not possible, then I hope that this question helps people in the same situation getting started faster with their testing framework.


3 Answers 3


I'm relatively new to this but from all the tutorials I've seen with respect truffle and unit testing, they all tend to use the Mocha and Chai testing framework. I would recommend looking into those two libraries. There's lots of tutorials on them online.

Take a look at this tutorial from DAPP University. He does a pretty good explanation of how to use the libraries for testing. (Unit testing starts around the 47 minute mark)


There are several out there so feel free to use a different one. In all the tutorials I've watched I can't recall ever seeing someone put a unit test directly into a solidity smart contract. Normally they're either a javascript file or python file that interacts with the smart contract.

By moving your test code out of solidity and into a .js or .py file you should greatly reduce your gas fees. (I imagine this would help with your memory issues too, but like I said I'm pretty new to this and have never seen someone try to perform a unit test within solidity.)


I'm currently going through this FreeCodeCamp tutorial, which has several examples of both unit and staging testing. They use Hardhat, Chai, ethers, Waffle, and Mocha. Highly recommend. Very efficient teaching. https://youtu.be/gyMwXuJrbJQ.

  • Link only answers are discouraged. It is better to include the main idea with the answer and use the link to expand on the details.
    – Ismael
    Commented Jul 31, 2022 at 23:23
  • Makes total sense. Apologies. I think I was going too fast and read through the question too fast. Thanks for the feedback, and I'll incorporate in future.
    – KidAmulet
    Commented Aug 1, 2022 at 12:12

For Remix Unit Tests you need to use the items in the comments for switching accounts and sending eth from them within test functions.

    /// #value: 1000000000000000000
    /// #sender: account-2
    function f() public payable {
  • I started with this since it is the official approach. Unfortunately this is not really feasible if you want to test actual interactions. It leads to dozens of unreadable functions. And, if I remember right, you pretty quickly run into gas limit problems. Try for instance to write my example this way. That's why my first requirement was "test different actors". Commented Nov 13, 2022 at 13:48
  • 1
    I am using HardHat now and have written extensive tests for my smart contracts. Remix was too limited in the end.
    – Jonathan
    Commented Nov 16, 2022 at 5:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.