Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Ethereum: Test-driven development with Solidity (part 1)

Depends on how you count, second and third generation blockchain applications are not bound by restrictions of underlying protocols. Programmers can create smart contracts — distributed applications with access to code-controlled accounts. Use cases go far beyond exchanging value and applies where users benefit from replacing trust between parties with code.

Ethereum blockchain is a decentralized platform which provides a runtime environment for smart contracts called Ethereum Virtual Machine (EVM). Contracts are completely isolated with limited access to other smart contracts. If you are new in this space, for now, you can think about Ethereum as a slow but reliable and secure computer.

In this post, I would like to introduce you to Solidity through TDD approach. Solidity is a contract-oriented, high-level programming language. If you are new to Solidity, this might be a little challenge. On the other hand, if you are already familiar with creating smart contracts, I hope this post will help you get better at testing. I recommend you to try CryptoZombies if you find yourself confused with Solidity code in this post.

There are a few reasons why I find testing smart contracts very important and interesting. You can choose Solidity or JavaScript as a language for writing tests. You can also write in both! Depends on what you would like to test, one testing environment will prove superior to another.

Smart contracts are not a good fit for “move fast and break things” kind of mindset. Blockchain is immutable in the sense that a once approved transaction is going to stay. Since smart contract deployment happens through a transaction, this results in the inability to fix issues quickly. That is why having a reliable set of tests is so crucial. There are of course different techniques which allow you to introduce escape hatches and migrate to a new, improved version of the smart contract. It comes with a set of potential security vulnerabilities. Contracts upgradeability gives too much power in the hands of an owner of the contract. This raises a question about real decentralization of the app.

On GitHub (MichalZalecki/tdd-solidity-intro) you can find the full source code. After you finish, do not forget about reading the second part!

Setup

The easiest way to start with smart contracts development for Ethereum is through Remix, an online IDE. It does not require any setup and integrates nicely with MetaMask to allow deploying contracts to a particular network. Despite all that I am going with Truffle.

Truffle is a popular development framework written in JavaScript. It neither comes with any convention for your smart contracts nor utility library but provides you with a local environment for developing, testing and deploying contracts. For starters, install truffle locally.

npm i truffle

Truffle makes it possible to kick off the project using one of many boxes. A box is a boilerplate containing something more than just a necessary minimum. We are interested in starting a new project from scratch.

./node_modules/.bin/truffle init

Look around; there is not much there yet. The only interesting bit is a Migrations.sol contract and its migration file. History of migrations you are going to make over time is recorded on-chain through a Migrations contract.

Solidity is not the only language in which one can create a smart contract on EVM. Solidity compiles directly to EVM bytecode. There’s also LLL (low level, 1 step above EVM bytecode) and Serpent (LLL’s super-set) which I would not recommend due to known security issues. Another language is Vyper which aims to provide better security through simplicity and to increase audibility of the smart contracts code. It is still in experimental phase.

Rules

We are going to build a contract which allows for funds raising. Mechanics are the same as a single Kickstarter campaign. There is a particular time to reach the goal. If this does not happen, donors are free to request a refund of transferred Ether. If a goal is reached, the owner of the smart contact can withdraw funds. We want to also allow for “anonymous” donation which is merely a transfer of funds to a smart contract. I am saying anonymous, but as all transactions, it is a publicly visible Ether transfer. We are just unable to refund those funds.

With clearly defined scope we can start implementing our smart contract.

Setting an owner

The first feature we want our smart contract to have is an owner. Before we start writing the first test let’s create an empty Funding contract so our tests can compile.

// contracts/Funding.sol
pragma solidity 0.4.24;

contract Funding {
}

Now, with an empty contract defined we can create a testing contract.

// test/FundingTest.sol
pragma solidity 0.4.24;

import "truffle/Assert.sol";
import "../contracts/Funding.sol";

contract FundingTest {
}

Now, run tests.

$ ./node_modules/.bin/truffle test
Compiling ./contracts/Funding.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./test/FundingTest.sol...
Compiling truffle/Assert.sol...


  0 passing (0ms)

Yay! If you got it right, contracts should compile without any errors. But we still don’t have any tests; we need to fix that. We want Funding to store an address of its deployer as an owner.

contract FundingTest {
  function testSettingAnOwnerDuringCreation() public {
    Funding funding = new Funding();
    Assert.equal(funding.owner(), this, "An owner is different than a deployer");
  }
}

Each smart contract has an address. An instance of each smart contract is implicitly convertible to its address and this.balance returns contract’s balance. One smart contract can instantiate another, so we expect that owner of funding is still the same contract. Now, to the implementation.

contract Funding {
  address public owner;

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

Since Solidity 0.4.22 constructors should be difined using the constructor syntax. Previously, the constructor of the contract had to have the same name as the contract. A sender of the message inside the constructor is a deployer. Let’s rerun the tests!

FundingTest
    ✓ testSettingAnOwnerDuringCreation (64ms)


  1 passing (408ms)

We can create an equivalent test in JavaScript.

// test/FundingTest.js
const Funding = artifacts.require("Funding");

contract("Funding", accounts => {
  const [firstAccount] = accounts;

  it("sets an owner", async () => {
    const funding = await Funding.new();
    assert.equal(await funding.owner.call(), firstAccount);
  });
});

In JavaScript, we can require a contract using artifacts.require. Instead of describe which you may know from other testing frameworks we use contract which does some cleanup and provides a list of available accounts. The first account is used by default during tests.

FundingTest
    ✓ testSettingAnOwnerDuringCreation (66ms)

  Contract: Funding
    ✓ sets an owner (68ms)


  2 passing (551ms)

Apart from creating a new contract during tests, we would also like to access contracts deployed through a migration.

import "truffle/DeployedAddresses.sol";

contract FundingTest {
  function testSettingAnOwnerOfDeployedContract() public {
    Funding funding = Funding(DeployedAddresses.Funding());
    Assert.equal(funding.owner(), msg.sender, "An owner is different than a deployer");
  }
}

It fails as we do not have any migration for our Funding contract.

// migrations/2_funding.js
const Funding = artifacts.require("./Funding.sol");

module.exports = function(deployer) {
  deployer.deploy(Funding);
};

We can now rerun tests.

  FundingTest
    ✓ testSettingAnOwnerDuringCreation (70ms)
    ✓ testSettingAnOwnerOfDeployedContract (63ms)

  Contract: Funding
    ✓ sets an owner (62ms)


  3 passing (744ms)

Accepting donations

Next feature on the roadmap is accepting donations. Let’s start with a test in Solidity.

contract FundingTest {
  uint public initialBalance = 10 ether;

  function testAcceptingDonations() public {
    Funding funding = new Funding();
    Assert.equal(funding.raised(), 0, "Initial raised amount is different than 0");
    funding.donate.value(10 finney)();
    funding.donate.value(20 finney)();
    Assert.equal(funding.raised(), 30 finney, "Raised amount is different than sum of donations");
  }
}

We use a unit called Finney. You should know that the smallest, indivisible unit of Ether is called Wei (it fits uint type).

  • 1 Ether is 10^18 Wei
  • 1 Finney is 10^15 Wei
  • 1 Szabo is 10^12 Wei
  • 1 Shannon is 10^9 Wei

Initially, a contract has no spare ethers to transfer so we can set an initial balance. Ten ether is more than enough. Let's write an equivalent JavaScript test.

const FINNEY = 10**15;

contract("Funding", accounts => {
  const [firstAccount, secondAccount] = accounts;

  it("accepts donations", async () => {
    const funding = await Funding.new();
    await funding.donate({ from: firstAccount, value: 10 * FINNEY });
    await funding.donate({ from: secondAccount, value: 20 * FINNEY });
    assert.equal(await funding.raised.call(), 30 * FINNEY);
  });
});

Implementation can be following.

contract Funding {
  uint public raised;
  address public owner;

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

  function donate() public payable {
    raised += msg.value;
  }
}

For now, it is everything to make tests pass.

  FundingTest
    ✓ testSettingAnOwnerDuringCreation (68ms)
    ✓ testSettingAnOwnerOfDeployedContract (63ms)
    ✓ testAcceptingDonations (80ms)

  Contract: Funding
    ✓ sets an owner (56ms)
    ✓ accepts donations (96ms)


  5 passing (923ms)

Now, we would like to keep track of who donated how much.

function testTrackingDonorsBalance() public {
  Funding funding = new Funding();
  funding.donate.value(5 finney)();
  funding.donate.value(15 finney)();
  Assert.equal(funding.balances(this), 20 finney, "Donator balance is different than sum of donations");
}

Testing with JavaScript gives us an ability to test for multiple different accounts.

it("keeps track of donator balance", async () => {
  const funding = await Funding.new();
  await funding.donate({ from: firstAccount, value: 5 * FINNEY });
  await funding.donate({ from: secondAccount, value: 15 * FINNEY });
  await funding.donate({ from: secondAccount, value: 3 * FINNEY });
  assert.equal(await funding.balances.call(firstAccount), 5 * FINNEY);
  assert.equal(await funding.balances.call(secondAccount), 18 * FINNEY);
});

For tracking a balance of particular user, we can use mapping. We have to mark a function as payable, so it allows users to send ethers along with function calls.

contract Funding {
  uint public raised;
  address public owner;
  mapping(address => uint) public balances;

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

  function donate() public payable {
    balances[msg.sender] += msg.value;
    raised += msg.value;
  }
}

By now, tests should pass.

  FundingTest
    ✓ testSettingAnOwnerDuringCreation (61ms)
    ✓ testSettingAnOwnerOfDeployedContract (65ms)
    ✓ testAcceptingDonations (97ms)
    ✓ testTrackingDonorsBalance (61ms)

  Contract: Funding
    ✓ sets an owner (51ms)
    ✓ accepts donations (96ms)
    ✓ keeps track of donator balance (134ms)


  7 passing (1s)

Time constraint

Our donors can now donate, but there is no time constraint. We would like users to send us some Ether but only until fundraising is finished. We can get a current block timestamp by reading now property. If you start writing tests in Solidity, you will quickly realize that there is no easy way to manipulate block time from the testing smart contract. There is also no sleep method which would allow us to set a tiny duration, wait for a second or two and try again simulating that time for donating is up.

The other solution would be to make it possible to set an address of the contract from which we read current timestamp. This way we could mock this contract in tests injecting it as a dependency.

// contracts/Clock.sol
pragma solidity ^0.4.17;

contract Clock {
  uint private timestamp;

  function getNow() public view returns (uint) {
    if (timestamp > 0) {
      return timestamp;
    }
    return now;
  }

  function setNow(uint _timestamp) public returns (uint) {
    timestamp = _timestamp;
  }
}

This is how we can implement a Clock contract. We would need to restrict changing the timestamp to the owner, but it is not that important right now. It is enough to make tests green.

function testFinishingFundRising() public {
  Clock clock = Clock(DeployedAddresses.Clock());
  Funding funding = new Funding(1 days, address(clock));
  Assert.equal(funding.isFinished(), false, "Is finished before time is up");
  clock.setNow(now + 1 days);
  Assert.equal(funding.isFinished(), true, "Is not finished before time is up");
}

After we have changed the timestamp of the Clock contract, fundraising is finished. After you add a new contract, you have to remember to migrate it.

// migrations/2_funding.js
const Funding = artifacts.require("./Funding.sol");
const Clock = artifacts.require("./Clock.sol");

const DAY = 3600 * 24;

module.exports = async function(deployer) {
  await deployer.deploy(Clock);
  await deployer.deploy(Funding, DAY, Clock.address);
};

Now to the implementation.

import "./Clock.sol";

contract Funding {
  [...]

  uint public finishesAt;
  Clock clock;

  function Funding(uint _duration, address _clockAddress) public {
    owner = msg.sender;
    clock = Clock(_clockAddress);
    finishesAt = clock.getNow() + _duration;
  }

  function isFinished() public view returns (bool) {
    return finishesAt <= clock.getNow();
  }

  [...]
}

Tests should now pass.

  FundingTest
    ✓ testSettingAnOwnerDuringCreation (86ms)
    ✓ testAcceptingDonations (112ms)
    ✓ testTrackingDonorsBalance (64ms)
    ✓ testFinishingFundRising (58ms)

  Contract: Funding
    ✓ sets an owner (64ms)
    ✓ accepts donations (115ms)
    ✓ keeps track of donator balance (165ms)


  7 passing (1s)

Although tests are passing, I would stop here for a moment. We had to create a separate contract, acting as a dependency, just to be able to test the implementation. I was proud of myself but taking into consideration that we have just added another attack vector I think this solution is somewhat dumb rather than smart.

End of part one

This concludes the first part of the introduction to TDD in Solidity. In the next article, we take a step back with our approach in testing time-related smart contracts. Apart from that, you will see how to test for errors. We will also complete the rest of Funding smart contract by adding withdrawal and refund features.

You did an excellent job building strong foundation for a smart contract. Grab a cup of coffee, do a few squats, and you are ready for the second part: Ethereum: Test-driven development with Solidity (part 2)!

On GitHub (MichalZalecki/tdd-solidity-intro) you can find the full source code of test suits and the smart contact.