5

Truffle offers many conveniences for writing tests for Ethereum smart contracts. The benefits include no need for a separate chain process like Ganache, complex automation of linking and deploying of library contracts like SafeMath However, there is an inverse of control and tests must be executed using truffle command and must follow Mocha/Chai pattern.

Are there alternatives for Truffle that would especially

  • Would use modern and standard test runners like Jest instead of a custom wrapper commands

  • Play nicely with TypeScript

  • Set up and tear down in-process blockchain easily

  • Support automatic compilation, linking and deployment of complex contracts like ones with SafeMath library

I found e.g. this example for Jest, but it is unfinished project. There is another one with Ganache and Jest, but very simplified and does not support linking contracts.

2 Answers 2

4

I found openzeppelin-test-environment solving my problem. It allows ephemeral Ethereum blockchain setup, contract deployment, etc. relatively easy.

Below is my original Truffle + TypeScript test translated to OpenZeppelin + Jest + power-assert.

Jest test

import assert = require('assert');

import { accounts, contract } from '@openzeppelin/test-environment';

import {
  BN,           // Big Number support
  constants,    // Common constants, like the zero address and largest integers
} from '@openzeppelin/test-helpers';

// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = new BN('93468683899196345527500000');

// Ethereum accounts used in these tests
const [
  deployer,  // Deploys the smart contract
  owner, // Owns the initial supply
  user2 // Random dude who wants play with tokens
] = accounts;

// Loads a compiled contract using OpenZeppelin test-environment
const Dawn = contract.fromArtifact('Dawn');

beforeEach(() => {
  // No setup
});

afterEach(() => {
  // No setup
});

test('The supply should match original token', async () => {
  const token = await Dawn.new(owner, { from: deployer });
  const supply = await token.totalSupply();

  // Big number does not have power-assert support yet - https://github.com/power-assert-js/power-assert/issues/124
  assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());
});

test("Token should allow transfer", async () => {
  const token = await Dawn.new(owner, { from: deployer });
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  await token.transfer(user2, amount, { from: owner });
  const balanceAfter = await token.balanceOf(user2);
  assert(balanceAfter.toString() == amount.toString());
});

test("Token tranfers are disabled after pause", async () => {
  const token = await Dawn.new(owner, { from: deployer });
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  // Pause
  await token.pause({ from: owner });
  assert(await token.paused());
  // Transfer tokens fails after the pause
  assert.rejects(async () => {
    await token.transfer(user2, amount, { from: owner });
  });
});


test("Token tranfers can be paused by the owner only", async () => {
  const token = await Dawn.new(owner, { from: deployer });
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  // Transfer tokens fails after the pause
  assert.rejects(async () => {
    await token.pause({ from: user2 });
  });
});

test("Token cannot be send to 0x0 null address by accident", async () => {
  const token = await Dawn.new(owner, { from: deployer });
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  assert.rejects(async () => {
    await token.transfer(constants.ZERO_ADDRESS, amount, { from: owner });
  });
});

Original Truffle test (Mocha + power-assert)

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

var assert = require('assert'); // Power assert https://github.com/power-assert-js/espower-typescript

// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = web3.utils.toBN('93468683899196345527500000');

contract("Dawn", ([deployer, user1, user2]) => {

  const tokenOwner = user1;

  it("should have total supply of 1ST token after deploy", async () => {

    const dawn = await Dawn.new(tokenOwner, { from: deployer });
    const supply = await dawn.totalSupply();

    assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());
  });

  it("should allow transfer", async () => {

    const dawn = await Dawn.new(tokenOwner, { from: deployer });
    const amount = web3.utils.toWei("1", "ether"); // 1 full token

    // Transfer tokens
    await dawn.transfer(user2, amount, { from: tokenOwner });
    const balanceAfter = await dawn.balanceOf(user2);
    assert(balanceAfter.toString() == amount.toString());
  });

  it("should not allow transfers after pause", async () => {

    const dawn = await Dawn.new(tokenOwner, { from: deployer });
    const amount = web3.utils.toWei("1", "ether"); // 1 full token

    // Pause
    await dawn.pause({ from: tokenOwner });
    assert(await dawn.paused());

    // Transfer tokens fails
    assert.rejects(async () => {
      await dawn.transfer(user2, amount, { from: user1 });
    });
  });

  it("should not allow pause by a random person", async () => {

    const dawn = await Dawn.new(tokenOwner, { from: deployer });

    // Transfer tokens fails
    assert.rejects(async () => {
      await dawn.pause({ from: user2 });
    });
  });


});

My package.json

{
  "name": "dawntoken",
  "version": "1.0.0",
  "description": "",
  "main": "truffle.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "generate": "truffle compile && typechain --target truffle './build/**/*.json'",
    "test": "jest",
    "tsc": "tsc --noEmit"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@openzeppelin/test-environment": "^0.1.3",
    "@openzeppelin/test-helpers": "^0.5.4",
    "@types/jest": "^25.1.3",
    "@types/power-assert": "^1.5.3",
    "babel-jest": "^25.1.0",
    "babel-preset-power-assert": "^3.0.0",
    "espower-typescript": "^9.0.2",
    "jest": "^25.1.0",
    "power-assert": "^1.6.1",
    "ts-jest": "^25.2.1",
    "ts-node": "^8.6.2",
    "typescript": "^3.8.3",
    "babel-core": "^6.26.3"
  },
  "dependencies": {
    "@truffle/hdwallet-provider": "^1.0.32",
    "bignumber.js": "^9.0.0",
    "dotenv": "^8.2.0",
    "ganache-core": "^2.10.2",
    "openzeppelin-solidity": "^2.5.0",
    "solc": "^0.6.3",
    "truffle": "^5.1.16",
    "web3": "^1.2.6"
  },
  "jest": {
    "verbose": true,
    "preset": "ts-jest",
    "testMatch": ["**/tests/*.ts"],
    "testEnvironment": "node",
    "globals": {
      "ts-jest": {
        "babelConfig": {
          "presets": [
            "power-assert"
          ]
        }
      }
    }
  }
}

For further information how to set up Jest + TypeScript + power-assert look here.

4
1

Another option is the hardhat toolkit and the @0xweb/hardhat plugin. It also fulfils all the options.

  • Would use modern and standard test runners like Jest instead of custom wrapper commands

    It doesn't depend on the test runner, so it can be used with any you prefer most.

  • Play nicely with TypeScript

    0xweb generates TypeScript client class for each contract, where the blockchain communication is encapsulated and you call the strongly typed methods.

  • Set up and tear down in-process blockchain easily

    Hardhat supports localhost and in-process blockchain. Per default in-process client is used (which is also encapsulated)

  • Support automatic compilation

    Run the compilation with --watch flag, and the *.sol files are compiled and *.ts classes are generated on source-file change.

For example, you have the ERC20 token contract in /contracts/MyToken.sol

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("My Project Token", "TKN") {
        _mint(msg.sender, 5e18);
    }
}

Start the compilation with npx hardhat compile --watch true, and you are ready to write the tests, here is my test runner, but you can use any other

import { MyToken } from '@0xweb/hardhat/MyToken/MyToken';
import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';

UTest({

    async 'should check totalSupply and the balance' () {

        let provider = new HardhatProvider();
        let token = await provider.deployClass(MyToken);

        let supply = await token.totalSupply();
        eq_(supply, 5n ** 18n);

        let account = provider.deployer();
        let balance = await token.balanceOf(account.address);
        eq_(balance, 5n ** 18n);
    }
});

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.