How to create tests for your Solidity Smart Contract
Tags: web3, solidity, ethereum, blockchain
Published: 2021-10-16 16:54:05

If you have followed all my previous blog posts and you had a peek at the code of all the contracts I have created you should already have seen that I always write tests for every smart contract I create.

Have you missed those projects? Well, don’t worry, here’s a list to refresh your memory:

-   [Scaffold-eth Challenge 1: Staking dApp](https://dev.to/stermi/scaffold-eth-challenge-1-staking-dapp-4ofb)
-   [How to create an ERC20 Token and a Solidity Vendor Contract to sell/buy your own token](https://dev.to/stermi/how-to-create-an-erc20-token-and-a-solidity-vendor-contract-to-sell-buy-your-own-token-4j1m)
-   [Unloot the Loot Project: my very first Solidity smart contract deployed on Ethereum](https://dev.to/stermi/unloot-the-loot-project-my-very-first-solidity-smart-contract-deployed-on-ethereum-1g2b)
-   [How to deploy your first smart contract on Ethereum with Solidity and Hardhat](https://dev.to/stermi/how-to-deploy-your-first-smart-contract-on-ethereum-with-solidity-and-hardhat-5efc)
-   [Proof of Concept of an Achievement System for Rarity](https://dev.to/stermi/proof-of-concept-of-an-achievement-system-for-rarity-4ia4)
- [Rarity Achievement System: update and release date](https://dev.to/stermi/rarity-achievement-system-update-and-release-date-49ai)

In each of those blog posts you have a GitHub repository where you can see the contract and test code, so don’t wait and give it a read before continuing!

## Why should you bother to write tests?

I know, you have  written your Solidity smart contract, you have already started the React dev server and you just want to interact with your smart contract deploying it to the main net yoloing everything.

I know that feeling, I know that excitement you get. And you could ask me “Tests are BORING!, why should I bother to write them? Let’s deploy things and see what happen”

> Nope, tests are not boring and they are not difficult to write if you know what you need to test and what your contract should and shouldn’t do!

Your smart contract when deployed is  **immutable**, remember that always. You need to be sure that things work as you expect when you deploy them. You can’t predict how others (users or contracts) will interact with it.

But tests come to the rescue you, and while you will write them (and I can assure you that you will write them fast) you will notice all the bugs, errors, and not implemented things you have missed while writing your code.

> It’s like reviewing something from a different point of view.

With tests you can:

-   Automate how accounts (wallets) and external smart contracts interact with your contract. Do you really want to do everything manually?
-   Check that calling a function with specific input gives the expected output: correct returned value and correct and correct contract’s state
-   Check that all the  `require`  statements work as expected
-   Check that  `events`  are correctly emitted
-   Check that user, contract and token  **balances correctly change**  after a transaction
-   Check that your last-minute change or a new function that interacts with other parts of the code does not break other tests.

## 🛠 Tools you should use when developing

Many common errors can be easily covered using specific tools that you should always have:

-   [Solhint](https://github.com/protofire/solhint/)
-   [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity)
-   [Solhint plugin Prettier](https://github.com/fvictorio/solhint-plugin-prettier)
-   [Hardhat Solhint](https://hardhat.org/plugins/nomiclabs-hardhat-solhint.html)
-   [solidity-coverage](https://hardhat.org/plugins/solidity-coverage.html)

If you want to know more about those tools and how to use them in your Solidity Hardhat project follow my previous post  [“How to deploy your first smart contract on Ethereum with Solidity and Hardhat”](https://stermi.medium.com/how-to-deploy-your-first-smart-contract-on-ethereum-with-solidity-and-hardhat-22f21d31096e).

Another great suggestion is to  **not reinvent the wheel**. If your contract is about creating a  **Token (ERC20)** or an  **NFT (ER721)** just use an  [OpenZeppelin](https://openzeppelin.com/contracts/)  contract. They provide secure and optimized implementations of those standards and you can be sure that they are more than battle-tested!

## What should you cover in tests?

Before starting writing test coverage I try to think about which tests I need to develop. This is my personal test checklist so it can differ between developers and developers but I think that it can be taken as a good start.

When you write tests you need to cover two main aspects:

-   Known bugs / exploits / common errors
-   Check that for each of your functions that are externally or publicly accessible given a specific input you get the output you expect. When I say output it can be both a state change of your contract or values returned by your function. If some of these functions break people could burn all the transaction gas, lose money or get the NFT stuck forever.

### Cover Known bugs/exploits/common errors

This part is both easy and hard at the same time. It’s easy because these errors that your smart contract code needs to cover are already known. The hard part is to know all of them and remember where in your code they could happen.

I have collected some really good content about ethereum and smart contract security and best practice. I strongly suggest you to

-   [Secureum](https://twitter.com/thesecureum?lang=en)  on Twitter and its founder  [0xRajeev](https://twitter.com/0xRajeev)  and their  [newsletter archive](https://secureum.substack.com/archive)
-   [Solidity by Example](https://solidity-by-example.org/)  has a list of Hacks
-   [OpenZeppelin Smart Contract Security Guidelines](https://blog.openzeppelin.com/smart-contract-security-guidelines/)
-   [Ethereum smart contracts security recommendations and best practices by Guy Lando](https://github.com/guylando/KnowledgeLists/blob/master/EthereumSmartContracts.md)
-   [Known Attacks](https://consensys.github.io/smart-contract-best-practices/known_attacks/)  and  [Secure Development Recommendations](https://consensys.github.io/smart-contract-best-practices/recommendations/)  by Consensys
-   [Ethereum Smart Contract Security Best Practices](https://consensys.github.io/smart-contract-best-practices/)  by Consensys

### Cover the implementation of your own functions

When you start writing tests you need to have in mind very clear which are the actors, which is the context, which is the state of the world before the transaction and after a transaction.

For example:

-   You have implemented an NFT contract and at mint time you want to limit people to mint only 2 NFT per transaction with a total of 10 NFT per account.

In this case:

-   Actors: User’s wallet and Contract’s wallet
-   Context: NFT sale
-   State before: user has  _X1_  ETH and  _Y1_  NFT, the contract has  _Z1_  ETH and  _W1_  NFT
-   State after (if everything goes well): user has  _X2_  ETH and  _Y2_  NFT, the contract has  _Z2_  ETH and  _W2_  NFT

When you write a test for the implementation of your own functions you need to start answering these questions:

-   Have I covered all the  **edge cases**?
-   Is the function  **reverting**  when expected?
-   Is the function  **emitting**  the needed  **events**?
-   With a specific  **input**, will the function produce the expected  **output**? Will the new  **state**  of the Contract be shaped as we expect?
-   Will the function  **returns**  (if it returns something) what we are expecting?
-   Has the user’s wallet and contract’s wallet  **changed their value**  as expected after the transaction

### Some useful tips

I’ll list some useful concepts and functions that I created while I was writing tests for different smart contracts.

**How to simulate blockchain mining in your test**

When you create unit testing you are using Hardhat local blockchain and not a “real” one where people mint blocks every X seconds. Locally you are the only one interacting with the blockchain and it means that you are the only one creating transactions and minting blocks. It could happen that you need to simulate “time passing” because you need to make some checks on the  `block.timestamp`  . The timestamp of the block is updated only if a transaction happens.

To solve this issue on our test I have implemented a little utility:

![Increase local blockchain timestamp](https://miro.medium.com/max/700/1*yUtwPqTpi2SYJV7W44qzWA.png)

When you call`increaseWorldTimeInSeconds(10, true)`  it will increase the EVM internal timestamp 10 seconds ahead of the current time. After that, if you specify it, it will also mine a block to create a transaction.

The next time that your contract will be called the  `block.timestamp`  should be updated.

**How to impersonate an account or a contract**

In the latest contract, I needed to test an interaction from  `ContractA`  that was calling  `ContractB`. In order to do that I needed to impersonate ContractA

It can be easily be done like this

![Impersonate an account with hardhat](https://miro.medium.com/max/700/1*sBSqkyAMDAimdZe6F0lpzA.png)

## Waffle

![Waffle](https://miro.medium.com/max/300/1*34ZMJXV7u6GtPXhaE8KPnA.png)

[Waffle](https://ethereum-waffle.readthedocs.io/en/latest/index.html)  is a library for writing and testing smart contracts that work with ethers-js like a charm.

> _Waffle is packed with tools that help with that. Tests in waffle are written using_ [_Mocha_](https://mochajs.org/) _alongside with_ [_Chai_](https://www.chaijs.com/)_. You can use a different test environment, but Waffle matchers only work with_ `_chai_`_._

To test our contract we will use  [Chai matchers](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html)  that will verify that the conditions we are expecting have been met.

After you have written all your test you just need to type  `yarn test`  and all your tests will be automatically run against your contract.

### Chai matchers

Matchers are utilities that make your test easy to write and read. When you write tests usually you can think about them like this

`expect(SOMETHING_TO_EXPECT_TO_HAPPEN).aMatcher(HAPPENED);`

So you are  **expecting**  that something  **matches**  something else that has happened.

For example:

-   When you call  `contractA.emitEvent()`  it will  **emit**  a specific event  `NFTMinted`  with specific parameters
-   When you call  `contractA.mint(10)`  the transaction has been  **reverted**  because users are allowed to mint at max 2 NFT per transaction
-   and so on

I’ll list all the available matchers, go to the documentation to know how to use them:

-   [Bignumber](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#bignumbers): testing equality of two big numbers (equal, eq, above, gt, gte, below, lt, lte, least, most, within, closeTo)
-   [Emitting events](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#emitting-events): testing what events were emitted with what arguments. Only  `indexed`  event parameters can be matched
-   [Called on contract](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#called-on-contract): test if a specific function has been called on the provided contract. It can be useful to check that an external contract function has been correctly called. You can also check if that function has been called passing specific arguments.
-   [Revert](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#revert): test if a transaction has been reverted or has been reverted with a specific message.
-   [Change ether balance](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#change-ether-balance): test that after a transaction the balance of a wallet (or multiple one) has changed of X amount of ETH. Note that  `changeEtherBalance`  won’t work if there is more than one tx mined in the block and that the expected value ignores fee (gas) by default (you can change it via options)
-   [Change token balance](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#change-token-balance): same thing as the  `changeEtherBalance`  but this matcher tests that a specific token balance has changed of an X delta (instead of ETH).

### Advanced Waffle

Waffle offers much more than only matchers and you could build tests even without using hardhat as a local chain.

-   Creating a  [Mocked Provider](https://ethereum-waffle.readthedocs.io/en/latest/basic-testing.html#creating-a-provider)  for your tests
-   [Getting wallets](https://ethereum-waffle.readthedocs.io/en/latest/basic-testing.html#getting-wallets)  from a Mocked Provider
-   [Deploying a contract](https://ethereum-waffle.readthedocs.io/en/latest/basic-testing.html#deploying-contracts)
-   [Using fixtures](https://ethereum-waffle.readthedocs.io/en/latest/fixtures.html)  to speed up tests when you need to automate some tasks like deploying contracts and adding funds to them. Waffle execute once and then remember those scenarios by taking snapshots of the blockchain

## WorldPurpose Contract

I have created a basic smart contract in order to be able to write test coverage for it. The smart contract is called WorldPurpose and the scope for this contract is to allow people to set a World Purpose paying investment to be the one to decide which is the purpose for the whole of humanity. Everyone can override the world's purpose, you just need to invest more money. The previous owners of the purposes can withdraw their funds only when their purposes are overridden.

The most important method in this smart contract are:

-   `function setPurpose(string memory  __purpose_) public payable onlyNewUser returns (Purpose memory  _newPurpose_)`
-   `function withdraw() public`

Let’s see the entire code of the contract, we are going to discuss how to create a test later on

## Test Coverage

Let’s assume that you already have a working hardhat project, fully set up with all the libraries installed. If you don’t already have one just clone my  [solidity-template project](https://github.com/StErMi/solidity-template).

So first of let’s set up the test file. Usually, I create a test file for each contract. If the contract is big, it has a lot of functions and so a lot of tests maybe it could be a good thing to split each function’s test into different files, but this is just up to you and how you usually manage your project’s structure.

Let’s create a  `worldpurpose.js`  file inside our  `/test`  folder at the root of our project. If you want to use TypeScript (as I usually do, head over to the template project to see how to create tests with  [TypeChain](https://hardhat.org/plugins/typechain-hardhat.html)  and Typescript)

Inside of it, we are going to write this code that I’ll explain

`describe`  is a function that describes what the test is about, what we are going to be testing in this file. It’s useful to structure and read the code and organize the output of our shell when we will run the test.

`beforeEach`  is a function that is executed before every single test. In this case for each test that we add to our test coverage file, a new  `worldPurpose`  contract will be deployed. We want to do that because in this case we always want to start a test from a clean checkpoint (everything is reset).

Now, for every function, we are going to set up a new  `describe`  function. And inside of them, we will create a test to cover a specific scenario thanks to the function  `it`  that will run the test itself.

For example

```ts
describe('Test withdraw', () => {  
  it('You cant withdraw when your balance is empty', async () => {  
    const tx = worldPurpose.connect(addr1).withdraw();await expect(tx).to.be.revertedWith("You don't have enough withdrawable balance");  
  });  
});
```

Ok now that we know how to structure a test let’s review them.

### Create tests for the  _setPurpose_  function

If we review the solidity code of the function we see that:

-   it has a  `function modifier`  `onlyNewUser`  that check that the  `msg.sender`  is different from the current purpose owner. We don’t want that the owner can override his/her purpose
-   it requires that ether be sent (the method declared as  `payable`) is greater than the current purpose  `investment`  value
-   it requires that the  `string memory _purpose`  input parameter (the purpose value) is not empty

If everything passes these checks the function will

-   update the current purpose
-   track the investment of new purpose’s owner in a  `balances`  variables
-   emit a  `PurposeChange`  event
-   return the new  `purpose`  struct

For each of these requirements, state changes, event emitted, and returned value we need to create a test. Keep in mind that this is a “simple contract” without contract-to-contract interactions or complex logic.

These are the test we need to implement:

1.  ❌ user can’t override his/her own purpose
2.  ❌ user can set a purpose if the investment is 0
3.  ❌ purpose message must be not empty
4.  ❌ if there’s already a purpose and the user wants to override it, he/she must invest more than the current purpose’s investment
5.  ✅ user set a purpose successfully when there’s no current purpose
6.  ✅ user overrides the previous purpose
7.  ✅ setting a purpose correctly emit the  `PuposeChange`  event

Let’s review some of those. This is the code that covers the first scenario in the previous list:

```ts
it("You can't override your own purpose", async () => {  
  await worldPurpose.connect(addr1).setPurpose("I'm the new world purpose!", {  
    value: ethers.utils.parseEther('0.10'),  
  });const tx = worldPurpose.connect(addr1).setPurpose('I want to override the my own purpose!', {  
    value: ethers.utils.parseEther('0.11'),  
  });await expect(tx).to.be.revertedWith('You cannot override your own purpose');  
});
```

In this case, we want to test that if the current owner of the purpose tries to override his/her own purpose the transaction will be reverted.

Let’s review the code

`worldPurpose`  is the variable that contains the deployed contract (deployed by the  `beforeEach`  method before every test)

`worldPurpose.connect(addr1)`  allow you to connect to the contract with the wallet’s address of the account  `addr1`

await worldPurpose.connect(addr1).setPurpose("I'm the new world purpose!", {  
  value: ethers.utils.parseEther('0.10'),  
});

`addr1`  invoke the  `setPurpose`  function of the  `worldPurpose`  contract passing  `I’m the new world purpose!`  as  `_purpose`  input parameter. The  `value`  parameter is how much ether will be sent with the transaction. In this case  `0.10 ETH`

The second part of the test tries to repeat the same operation but we already know that it will fail because the same address cannot override the purpose.

We are going to use the **Waffle matcher**  to check that the transaction has been  **reverted**  with a  **specific error message**.

await expect(tx).to.be.revertedWith('You cannot override your own purpose');  
});

Easy right?

**Now it’s your turn to write all the other reverting tests that need to be covered.**

Let’s see the code of a “success” scenario (the fifth one on the previous list) where we want to cover the case where the user successfully overrides a purpose.

```ts
it("set purpose success when there's no purpose", async () => {  
  const purposeTitle = 'Reduce the ETH fee cost in the next 3 months';  
  const purposeInvestment = ethers.utils.parseEther('0.1');  
  await worldPurpose.connect(addr1).setPurpose(purposeTitle, {  
    value: purposeInvestment,  
  });// Check that the purpose has been set  
  const currentPurpose = await worldPurpose.getCurrentPurpose();  
  expect(currentPurpose.purpose).to.equal(purposeTitle);  
  expect(currentPurpose.owner).to.equal(addr1.address);  
  expect(currentPurpose.investment).to.equal(purposeInvestment);// Check that the balance has been updated  
  const balance = await worldPurpose.connect(addr1).getBalance();  
  expect(balance).to.equal(purposeInvestment);  
});
```

We have already explained the first part where the  `addr1`  call the  `setPurpose`  function.

Now we want to be sure that the purpose has been written into the contract’s state and that the user’s investment has been tracked correctly into the  `balances`  variable.

We call the  `getCurrentPurpose`  getter function to get the current purpose of the contract and for each member of the  `struct Purpose`  we check that the  **value**  is  **equal** (`.to.be`)  to the one we  **expect**

After that, we check that the balance of  `addr1`  (`worldPurpose.connect(addr1).getBalance()`) is equal to the amount of ether we have sent with the transaction.

We could also check that the event  `PurposeChange`  has been emitted by the function (in the code we are doing it in another test). To do that we can write

await expect(tx).to.emit(worldPurpose, 'PurposeChange').withArgs(addr1.address, purposeTitle, purposeInvestment);

### Create tests for the withdraw function

We calculate the  `withdrawable`  amount by the  `msg.sender`. If the sender is also the owner of the current world’s purpose we subtract the  `purpose.investment`  from the  `balances[msg.sender]`.

With that in mind we need to do some checks:

-   `withdrawable`  the amount must be greater than zero
-   `msg.sender.call{value: withdrawable}("")`  must return success (user has withdrawn successfully the amount)

If everything passes these checks the function will

-   update the user’s balance to  `balances[msg.sender] -= withdrawable`

These are the test we are going to implement

1.  ❌ user can’t withdraw because he has an empty balance (never set a purpose)
2.  ❌ user can’t withdraw because he’s the current owner of the purpose. his/her  `withdrawable`  amount is 0
3.  ✅ user1 has set a purpose, someone else has overridden the purpose so user1 can withdraw the whole amount
4.  ✅ user1 has set a purpose, someone else has overridden it but user1 set a new purpose for the second time. In this case, he can withdraw only the first purpose investment

Let’s review the code of the success scenario (the third one in the previous list) and you will be in charge to implement the other tests.

```ts
it('Withdraw your previous investment', async () => {  
  const firstInvestment = ethers.utils.parseEther('0.10');  
  await worldPurpose.connect(addr1).setPurpose('First purpose', {  
    value: ethers.utils.parseEther('0.10'),  
  });await worldPurpose.connect(addr2).setPurpose('Second purpose', {  
    value: ethers.utils.parseEther('0.11'),  
  });const tx = await worldPurpose.connect(addr1).withdraw();// Check that my current balance on contract is 0  
  const balance = await worldPurpose.connect(addr1).getBalance();  
  expect(balance).to.equal(0);// Check that I got back in my wallet the whole import  
  await expect(tx).to.changeEtherBalance(addr1, firstInvestment);  
});
```

We have already seen the  `setPurpose`  function so the first part should be pretty straightforward.

`addr1`  set a purpose with  `0.10`  ETH investment,  `addr2`  override the purpose of investing  `0.11`  ETH

`addr1`  call the  `withdraw()`  function. The contract checks how much he can withdraw and send the amount back to his/her wallet.

In this test scenario, we check that after the transaction has been minted:

-   the  `getBalance`  the function of the contract must return 0 (he/she has withdrawn all the balance)
-   the transaction should have changed the ether balance of the  `addr1`  wallet of the same amount he has invested when he/she has invoked the  `setPurpose`, in this case  `0.10`  ETH

Now it’s your turn to implement the remaining tests!

## Run all tests together

When you have finished writing down all your tests just run this command to run them  `npx hardhat test`. You should see a result similar to this:

![Test result](https://miro.medium.com/max/564/1*Xz6qvvyqPcGV8WTAKwCXtA.png)

Well done! You have just created your first test file for your solidity project!

## Did you like this content? Follow me for more!

-   GitHub:  [https://github.com/StErMi](https://github.com/StErMi)
-   Twitter:  [https://twitter.com/StErMi](https://twitter.com/StErMi)
-   Medium:  [https://medium.com/@stermi](https://medium.com/@stermi)
-   Dev.to:  [https://dev.to/stermi](https://dev.to/stermi)