Ethereum smart contracts

Testing Time-Based Ethereum Smart Contracts in Solidity Without a Test Suite

When I first got started with Solidity, I immediately went down a rabbit-hole of “How do I write tests for this?” and found a lot of answers, all of which required fairly heavy-handed setup for someone just starting out. So I did what everyone else does: I started manually deploying contracts to the testnet with Ethererum Wallet and manually invoking their public functions.

This soon becomes annoying, repetitive, and the biggest bottleneck in getting your contract written. Obviously, longer-term, you want to be a good citizen and set up a test suite. When you’re just hacking around on something for fun, however, that’s kind of a drag.

Even worse, if your contract revolves around time as a key mechanic, you’ll find yourself waiting for extended periods of time or frequently missing important time windows as you switch back and forth from code editor to wallet.

By the way, we’re hiring at Revelry.

If you love open source software, have a look in our library and come to play.
Here’s our library.
Check out our Careers page for details.

Assumptions about the reader

You already know how to use Ethereum Wallet, and you already know how to write Solidity code using inheritance.

I devised a strategy for quicker semi-manual testing for Ethereum Smart Contracts:

  1. Refactor time-based code to make the concept of “now” overridable by child contracts.
  2. Create a child contract that makes a series of assertions in its constructor.
  3. Start to deploy the test contract in Ethereum Wallet.
  4. If Ethereum Wallet complains that execution will fail, your tests failed. If it doesn’t, they succeeded.

Warning: now is an alias for block.timestamp, which is known to be cheatable within shorter time windows. Please seek professional guidance before using it in financially important code.

 

Let’s get to it…

This is a contract Lockbox with behavior based upon the current block time. The owner can only withdraw at or after a specified time. It’s a little annoying to test.

pragma solidity ^0.4.11;

contract Lockbox {
  address owner;
  uint unlockTime;

  function Lockbox(uint _unlockTime) {
    owner = msg.sender;
    unlockTime = _unlockTime;
  }

  function isUnlocked() internal returns (bool) {
    return now >= unlockTime;
  }

  modifier onlyOwner() { require(msg.sender == owner); _; }
  modifier onlyWhenUnlocked() { require(isUnlocked()); _; }

  function withdrawBalance() payable onlyOwner onlyWhenUnlocked {
    owner.transfer(address(this).balance);
  }
}

Since now is a global constant, our time-based behavior is hard to deal with in tests. Let’s refactor a little so we have a function we can override. In practice, you would probably just modify the Lockbox code. For brevity and clarity, I’m going to show my modifications in a child contract:

contract EasierToTestLockBox is Lockbox {
  function EasierToTestLockBox(uint _unlockTime) Lockbox(_unlockTime) {}

  // Make the concept of "now" overridable.

  function getTime() internal returns (uint) {
    return now;
  }

  // Replace "now" references with "getTime()".

  function isUnlocked() internal returns (bool) {
    return getTime() >= unlockTime;
  }
}

Now that we can write a child that manipulates time, we can test against arbitrary times. No more waiting around for actual block time to pass. We just set the clock to whatever we want it to be.

contract LockboxTest is EasierToTestLockBox(now + 1 minutes) {

  // Add a manipulable fake clock to the contract.

  uint fakeClockTime = now;

  // Override the concept of "now" to use our fake clock.

  function getTime() internal returns (uint) {
    return fakeClockTime;
  }

  // Test in the constructor.
  // If a `require` fails, Ethereum Wallet will warn you before you deploy.

  function LockboxTest() {
    require(!isUnlocked());
    fakeClockTime = unlockTime;
    require(isUnlocked());
  }
}

Open up Ethereum Wallet, paste in the contract code, select “Lockbox Test”, and click “Deploy.” If you see this, then something in your test failed.

Screen shot 2017-06-25 at 12 09 41 pm

If not, good to go! Of course, this doesn’t tell you what went wrong, just that something did. For that, we could create assertion functions that log events instead of aborting the transaction, but I’ll leave that as an exercise for the reader.

This is a quick and dirty way to reclaim some of your hobby time, after all. At a certain point, you’ll just need to bite the bullet and get yourself a test suite. For serious business we recommend Truffle Framework.

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.