Truffle Alternatives – Best Alternatives for Running and Writing Tests

testingtestrpctruffletypescript

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.

Best Answer

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.