Building a full-stack Decentralised Application on the Ethereum blockchain
Part Two: Testing the smart contract
This blog series is broken down into five parts:
In this section we’ll be testing our smart contract with the JavaScript testing frameworks Waffle & Chai. It’s imperative to test our smart contract before we deploy it, especially when we begin making more complicated smart contracts where there are transactions of tokens involved.
The immutability of smart contracts (the inability to change them) once they’ve been live is a large factor in making sure that our smart contracts are rigorously tested before they are deployed. There’s also been an explicit focus put on rigorous smart contract testing due to large scale smart contract exploits and glitches in the past that have led to the theft or loss of huge sums of tokens. One of the most prominent examples of this was with an exploit found in ‘The DAO’ which ultimately caused a hard fork in the Ethereum blockchain in an effort to retrieve the stolen Ethereum tokens.
Installing dependencies
Before we can get started we need to install a few more dependencies that will help us with our testing. In your terminal, post the following command:
npm install --save-dev @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers ethereum-waffle chai ethers solidity-coverage
This will install the following dependencies:
@nomiclabs/hardhat-waffle: A hardhat plugin that enables waffle support.
@nomiclabs/hardhat-ethers: A hardhat plugin that enables ethers support.
ethereum-waffle: A Solidity testing library that will allow us to write tests for our smart contract using JavaScript.
chai: An assertion library that will allow us to use functions in our testing like expect().
ethers: An ethereum client library that allows you to interface with blockchains that implement the ethereum API.
solidity-coverage: This library provides coverage reports on the unit tests we write.
Activating our new plugins
To activate our plugins, we need to tell Hardhat to actually use them within our hardhat.config.js file. All we need to do is add the following two lines of code to the top of the file:
require("@nomiclabs/hardhat-waffle"); require('solidity-coverage');
Now we need to add a new script to our package.json file to test our smart contract. Open the the file and replace the existing test script line just above our build script with the following:
"test": "hardhat coverage",
Now when we call npm run test in our terminal this command will invoke Waffle and will also create a coverage report of our tests for us.
Writing tests
Now we’re all set to begin writing tests for our smart contract. Go ahead and create a new test folder in the root directory of our project and then create a new file within it called inbox.test.js. Within our new file, copy the following code:
const { expect } = require("chai"); describe("Inbox", () => { it("should return its stored message", async () => { const Inbox = await ethers.getContractFactory("Inbox"); const inbox = await Inbox.deploy("Perry Fardella was here!"); await inbox.deployed(); expect(await inbox.getMessage()).to.equal("Perry Fardella was here!"); }); it("should change its stored message when requested", async () => { const Inbox = await ethers.getContractFactory("Inbox"); const inbox = await Inbox.deploy("Perry Fardella was here!"); await inbox.setMessage("Perry Fardella was here, again!"); expect(await inbox.getMessage()).to.equal("Perry Fardella was here, again!"); }); });
Once again, we’re going to step through and break down this code line for line so we can wrap our heads around what this test file is doing.
const { expect } = require("chai");
This is a basic import statement which tells our file to import the expect method which we will use within our test from the chai library.
describe("Inbox", () => {
This is a describe block, it’s a top-level logical block within a test file that helps us separate out our tests and isolate where issues are. In this instance we’re saying that all tests within this particular describe block relate to the Inbox contract and its functionality.
it("should return its stored message", async () => {
This is an it block, it is also a logical block at a level just below our describe block and just like a describe block it helps to separate out our tests. An it block is generally used to test one specific method or functionality. In conjunction with a describe block, the it block helps to provide some natural language to our tests so they’re largely human-readable, even for those without any programming experience.
const Inbox = await ethers.getContractFactory("Inbox");
This line tells the ethers plugin that we just installed to look up our smart contract and create a factory so that we can later instantiate it as we need.
const inbox = await Inbox.deploy("Perry Fardella was here!");
This line deploys our smart contract and calls it’s constructor by setting the message variable to “Perry Fardella was here!”. I’d encourage you to use your own name for this part of the test instead of my own.
await inbox.setMessage("Perry Fardella was here, again!");
This line calls the setMessage method of our smart contract, changing the stored message variable within it.
expect(await inbox.getMessage()).to.equal("Perry Fardella was here!");
This line is where the actual checking occurs, it calls the getMessage method of our smart contract and makes sure that the value of the returned variable is as we expected it to be.
Running our tests
Now our tests are complete, it's time to run them and see if they pass!
Enter the following command into your terminal:
npm run test
Side bar: Error in plugin?!
At this point if you’re using a relatively new version of node, like v17.2.0 which I’m using, you may in fact encounter an error that looks something like this:
Error in plugin solidity-coverage: Error: error:0308010C:digital envelope routines::unsupported
This appears to be caused by a webpack error that stems from nodejs v17 and above introducing OpenSSL3. The workaround I’ve found to use is to run the following command in your terminal:
export NODE_OPTIONS=--openssl-legacy-provider
This will tell node to use a legacy version of OpenSSL which should resolve our error.
Expected output
If you didn’t have the error above or have now fixed it and re-run npm run test you should see an output that looks something like this:
Version ======= > solidity-coverage: v0.7.17 Instrumenting for coverage... ============================= > Inbox.sol Compilation: ============ Nothing to compile Network Info ============ > HardhatEVM: v2.7.1 > network: hardhat Inbox ✓ should return its stored message (65ms) ✓ should change its stored message when requested (38ms) 2 passing (106ms) ------------|----------|----------|----------|----------|----------------| File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | ------------|----------|----------|----------|----------|----------------| contracts/ | 100 | 100 | 100 | 100 | | Inbox.sol | 100 | 100 | 100 | 100 | | ------------|----------|----------|----------|----------|----------------| All files | 100 | 100 | 100 | 100 | | ------------|----------|----------|----------|----------|----------------| > Istanbul reports written to ./coverage/ and ./coverage.json
All our tests have now passed. This means our smart contract is operating as expected and specifically our constructor and getter and setter methods are all working properly. Now, it’s time to deploy our smart contract into the world!