Testing simple smart contract with Waffle library
In this tutorial you'll learn how to
- Test the changes of wallet balance
- Test emission of events with specified arguments
- Assert that a transaction was reverted
Assumptions
- You can create a new JavaScript or TypeScript project
- You have some basic experience with tests in JavaScript
- You have used some package managers like yarn or npm
- You possess very basic knowledge of smart contracts and Solidity
Getting started
The tutorial demonstrates test setup and run using yarn, but there is no problem if you prefer npm - I will provide proper references to the official Waffle documentation(opens in a new tab).
Install Dependencies
Add(opens in a new tab) ethereum-waffle and typescript dependencies to the dev dependencies of your project.
yarn add --dev ethereum-waffle ts-node typescript @types/jest
Example smart contract
During the tutorial we'll work on a simple smart contract example - EtherSplitter. It does not much apart from allowing anyone to send some wei and split it evenly between two predefined receivers. The split function requires the number of wei to be even, otherwise it will revert. For both receivers it performs a wei transfer followed by emission of the Transfer event.
Place the snippet of EtherSplitter code in src/EtherSplitter.sol
.
1pragma solidity ^0.6.0;23contract EtherSplitter {4 address payable receiver1;5 address payable receiver2;67 event Transfer(address from, address to, uint256 amount);89 constructor(address payable _address1, address payable _address2) public {10 receiver1 = _address1;11 receiver2 = _address2;12 }1314 function split() public payable {15 require(msg.value % 2 == 0, 'Uneven wei amount not allowed');16 receiver1.transfer(msg.value / 2);17 emit Transfer(msg.sender, receiver1, msg.value / 2);18 receiver2.transfer(msg.value / 2);19 emit Transfer(msg.sender, receiver2, msg.value / 2);20 }21}Onyesha yoteNakala
Compile the contract
To compile(opens in a new tab) the contract add the following entry to the package.json file:
1"scripts": {2 "build": "waffle"3 }Nakala
Next, create the Waffle configuration file in the project root directory - waffle.json
- and then paste the following configuration there:
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./src",5 "outputDirectory": "./build"6}Nakala
Run yarn build
. As the result, the build
directory will appear with the EtherSplitter compiled contract in JSON format.
Test setup
Testing with Waffle requires using Chai matchers and Mocha, so you need to add(opens in a new tab) them to your project. Update your package.json file and add the test
entry in the scripts part:
1"scripts": {2 "build": "waffle",3 "test": "export NODE_ENV=test && mocha -r ts-node/register 'test/**/*.test.ts'"4 }Nakala
If you want to execute(opens in a new tab) your tests, just run yarn test
.
Testing
Now create the test
directory and create the new file test\EtherSplitter.test.ts
.
Copy the snippet below and paste it to our test file.
1import { expect, use } from "chai"2import { Contract } from "ethers"3import { deployContract, MockProvider, solidity } from "ethereum-waffle"4import EtherSplitter from "../build/EtherSplitter.json"56use(solidity)78describe("Ether Splitter", () => {9 const [sender, receiver1, receiver2] = new MockProvider().getWallets()10 let splitter: Contract1112 beforeEach(async () => {13 splitter = await deployContract(sender, EtherSplitter, [14 receiver1.address,15 receiver2.address,16 ])17 })1819 // add the tests here20})Onyesha yote
A few words before we start.
The MockProvider
comes up with a mock version of the blockchain. It also delivers mock wallets that will serve us for testing EtherSplitter contract. We can get up to ten wallets by calling getWallets()
method on the provider. In the example, we get three wallets - for the sender and for two receivers.
Next, we declare a variable called 'splitter' - this is our mock EtherSplitter contract. It is created before each execution of a single test by the deployContract
method. This method simulates deployment of a contract from the wallet passed as the first parameter (sender's wallet in our case). The second parameter is the ABI and the bytecode of the tested contract - we pass there the json file of the compiled EtherSplitter contract from the build
directory. The third parameter is an array with the contract's constructor arguments, which in our case, are the two addresses of the receivers.
changeBalances
First, we will check if the split method actually changes the balances of receivers' wallets. If we split 50 wei from senders account, we would expect the balances of both receivers to increase by 25 wei. We will use Waffle's changeBalances
matcher:
1it("Changes accounts balances", async () => {2 await expect(() => splitter.split({ value: 50 })).to.changeBalances(3 [receiver1, receiver2],4 [25, 25]5 )6})
As the first parameter of the matcher, we pass an array of receivers' wallets, and as the second - an array of expected increases on corresponding accounts.
If we wanted to check the balance of one specific wallet, we could also use changeBalance
matcher, which does not require passing arrays, as in the example below:
1it("Changes account balance", async () => {2 await expect(() => splitter.split({ value: 50 })).to.changeBalance(3 receiver1,4 255 )6})
Note that in both cases of changeBalance
and changeBalances
we pass the split function as a callback because the matcher needs to access the state of balances before and after the call.
Next, we'll test if the Transfer event was emitted after each transfer of wei. We'll turn to another matcher from Waffle:
Emit
1it("Emits event on the transfer to the first receiver", async () => {2 await expect(splitter.split({ value: 50 }))3 .to.emit(splitter, "Transfer")4 .withArgs(sender.address, receiver1.address, 25)5})67it("Emits event on the transfer to the second receiver", async () => {8 await expect(splitter.split({ value: 50 }))9 .to.emit(splitter, "Transfer")10 .withArgs(sender.address, receiver2.address, 25)11})Onyesha yote
The emit
matcher allows us to check if a contract emitted an event on calling a method. As the parameters to the emit
matcher, we provide the mock contract that we predict to emit the event, along with the name of that event. In our case, the mock contract is splitter
and the name of the event - Transfer
. We can also verify the precise values of arguments that the event was emitted with - we pass as many arguments to withArgs
matcher, as our event declaration expects. In case of EtherSplitter contract, we pass the addresses of the sender and the receiver along with the transferred wei amount.
revertedWith
As the last example, we'll check if the transaction was reverted in case of uneven number of wei. We'll use revertedWith
matcher:
1it("Reverts when Vei amount uneven", async () => {2 await expect(splitter.split({ value: 51 })).to.be.revertedWith(3 "Uneven wei amount not allowed"4 )5})
The test, if passed, will assure us that the transaction was reverted indeed. However, there must be also an exact match between the messages that we passed in require
statement and the message we expect in revertedWith
. If we go back to the code of EtherSplitter contract, in the require
statement for the wei amount, we provide the message: 'Uneven wei amount not allowed'. This matches the message we expect in our test. If they were not equal, the test would fail.
Congratulations!
You've made your first big step towards testing smart contracts with Waffle! You might be interested in other Waffle tutorials:
- Testing ERC20 with Waffle
- Waffle: Dynamic mocking and testing contract calls
- Waffle say hello world tutorial with hardhat and ethers
Uhariri wa mwisho: @nhsz(opens in a new tab), 27 Februari 2024