How to Write Better Truffle Tests for Solidity Smart Contracts

A helpful piece that expands on Dapp University’s ERC-20 tutorial.

Matt Carreon
5 min readJun 2, 2021

Those interested in the technical side of cryptocurrencies have probably stumbled upon Dapp University’s tutorial on making your own ERC-20. If not, check it out! It’s very informative and is a great way to get started with writing Solidity. I’m about half way through and wanted to document some of the improvements I’ve made to the code.

What did I implement?

  1. Utilized async/await to avoid long promise chains
  2. Broke tests down into more it() functions & used context() to group them
  3. Added chai’s beforeEach() to reduce repeating code
  4. Named my test accounts
  5. Switched to expect() instead of assert()

Source Code

Dapp University truffle test code

My truffle test code

Use these two links if you’d like to compare the two tests. I’m still working through the tutorial, so I’m not fully finished with the repository.

Utilizing async/await

If you aren’t familiar with async/await, I recommend reading the MDN Docs on them. Essentially, it is syntactic sugar for writing cleaner code with promises. I’ll be focusing on where it’s implemented specifically for truffle tests, and why I chose to do so. I won’t be going into the basics of using async/await.

Changing the context() & it() functions to async

In Dapp University’s tutorial, they have you write unit tests like this:

it('test title', function() {
// test code
})

However, since we’ll be using await inside the tests for our promises now, I changed it to this for both contexts and the unit tests (I think arrow functions look cleaner so I switched that as well):

context('context of tests', async () => {
it('test title', async () => {
// test code w/ await
})
})

You can compare the differences below, personally I think it’s much easier to read since you don’t have to keep track of what the promises are returning and passing into the next function.

My Test

My truffle test for ERC-20 token transfers

Dapp University Test

Dapp University’s truffle tests for ERC-20 token transfers

Challenges with using async/await

You may have noticed this odd-looking line in my first unit test:

Code expecting an error from an async function

Why am I using await on expect()?

Well, one problem I came across while using async/await was implementing a test that was expecting an error. At first, I tried using a try-catch block like this:

try {
await contractInstance.transfer.call(bob, 9999999);
} catch(error) {
expect(error.message.indexOf('revert').to.be.at.least(0);
}

However, this caused the test to pass when no error was thrown, meaning it passed the test if the require() statement was missing from the solidity contract. After that, I tried this:

expect(await contractInstance.transfer.call(bob, 9999999)).to.throw('revert');

This resulted in an error being thrown and not continuing on with evaluating the expectation.

I then found this stackoverflow post describing the exact solution I needed. It requires the npm package chai-as-promised to work. This let me write the tests expecting errors from functions I’d usually use await with. You’ll need to add chai-as-promised to your chai instance like so:

// importing solidity file, chai, and chai-as-promised
const DappToken = artifacts.require("DappToken.sol");
const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
// needed to use chai-as-promised
chai.use(chaiAsPromised);
const expect = chai.expect;
// const assert = chai.assert // this is used if you use assertions
// chai.should() // this is used if you use should instead
// contract testing code continues here...

Breaking Tests Down & Grouping Them With context()

I broke down the tests into separate it() functions because I felt like the original code by Dapp University was testing too many things inside one unit test, for example:

  1. Reverting a transaction that can’t happen due to insufficient balance in a wallet
  2. Returning true if the function runs successfully
  3. Checking to make sure the function adjusts wallet balances correctly

So, I separated them out into their own separate tests and grouped them with the context() function.

My Test

My code for erc-20 token transfer from github
My truffle tests for ERC-20 token transfer

Dapp University Test

Dapp University’s unit test for transferring tokens from their github
Dapp University’s truffle tests for ERC-20 token transfer

What does context() do?

context() allows you to group multiple unit tests into a common group, like the ‘with attempting transfer of tokens’ group I have above. This makes the output in your console much cleaner, and lets you be more granular with your testing.

Utilizing the beforeEach() function

If you looked at Dapp University’s source code, you may have noticed this line at the start of every test:

This allows them to access the deployed DappToken contract instance, and then run the functions inside the contract. This repeated code can be eliminated by utilizing the beforeEach() function that chai has to create a new contract instance before each test.

Now, I’m able to call contractInstance directly in my tests without defining it every time.

Why use new() and not deployed()?

From my understanding, deployed() will access the same deployed contract instance, whereas new() will create a new instance every time. According to multiple stackoverflow discussions, this can help avoid side-effects that may occur.

Discussion 1

Discussion 2

Why name your accounts?

I use the following line to name my accounts I use in tests, just to make things nicer to read and type! It can help to label them so you know what account is being sent to and from as well.

let [alice, bob, fromAccount, toAccount, spendingAccount] = accounts;

Expect vs Assert

This last one is more of an opinion on the style of tests, rather than a functional difference.

I like the way that expect() reads when going through the tests since it sounds more like natural language. Either work fine from what I’ve learned so far.

You can read more about their differences on the chai style page.

Conclusion

So far, Dapp University’s tutorial has been very informative. I wanted to document the changes I made to help others, as well as remind myself of what changes I made! Please let me know if there are other improvements I should consider.

Thank you for reading :)

--

--

Matt Carreon
Matt Carreon

Written by Matt Carreon

I'm a tech enthusiast who loves working with the latest technology. Currently studying blockchain & cryptocurrencies!