In the first part of these series we created and compiled our first contract inheriting from ERC20 open zeppelin contract.
In this section will start creating our first test. If you never have try it TDD, always there is a first time, test usually come first. In this case our first test will check if our contract has been deployed. Why work this way? you will be asking in my experience this approach allows you to think what are the key features in your program and test can help you to identify and strength all the edge cases where these features could eventually fail. These tests could grow with time to cover new features or edge cases and if somehow you need to make changes in your programs these tests still ensure all the features still works. The latter in the case of smart-contracts is way more difficult to apply because once deployed in the Mainnet will be not possible to change (things like proxies are beyond our current scope). Anyway, this immutability characteristic of smart-contracts it would be one more reason to really test your contracts all you can before deploy them. Well enough theory lest write our first test.
In the terminal we will create our test file.
touch test/glass-test.js
and we open it in VSC
code test/glass-test.js -r
In hardhat we use javascript (or maybe I should say Node.js to be more precise), I think typescript is also available but we will use the former.
In our empty file we write this first two lines.
const { expect } = require("chai");
const { ethers } = require("hardhat");
If you are not familiar with javascript like me (my previous experience is with languages like Python, C# and C++) what we are doing is this lines are importing chai and hardhat modules using require (If you remember from the first article we installed both with hardhat). Also we are using object destructuring to import the only certain parts of this modules. I let you a link to a really nice article is you want to know more about it: link
chai is our library for testing and ethers is the library we will be using to interact with Ethereum blockchain.
So let's continue with our test.
describe("Glass Contract", function () {
it("Should return our contract", async function () {
const GlassContract = await ethers.getContractFactory("Glass");
const glassContract = await GlassContract.deploy(21000000);
await glassContract.deployed();
expect(glassContract).to.exist;
});
});
Wow to many things to unpack (literally) here. Javascript in my experience is very fond of these nested functions so let's try to peel all the layers. In first place we have describe, this function is use in our test to group our tests, the first parameter is a string where we put the name of the object we are testing and followed by and anonymous function expression (more on type of functions in JS here but I suggest you go slowly in this one). And finally it functions which contain our individual tests. Inside our test we create using ethers library a Factory that we use to create our contract calling the method deploy of the factory. As you can see we have to provide an argument, this is the parameter indicating our initial supply for our token.
Inside of the test we are checking with expect that our glassContract variable return not null or undefined.
Well that's all, isn't it, well... probably you notice I'm skipping that async just before the function expression, and the awaits inside. With these keywords we are enabling asynchronous. What that's mean? Well think for a moment what we are doing here, we are deploying our smart-contract in a network and we don't obtain and immediate result with this keyword we wait till we get this result and then we continue. If you are not familiar with promises and asynchronous, take a look to this link.
So we finally we are done, don't we. Well, let's test it (what would be the point if we don't test our test). How we do that? from the console we type this.
npx hardhat test test/glass-test.js
Show we should get something like this in our terminal window
What happen here let's explain a little bit. Hardhat deploy our contract and then test it that our contract already exists. But where was deployed? Well hardhat comes with local built in network that is raised for our testing and later on shut it down. How convenient, isn't it?
Hardhat also give us the choice to do that in an already deployed local network or in live networks too. But let's go back for a moment to our tests. First thing is we really didn't do extrictly TDD because we really create our contract before the test but you get the idea we could do it in reverse and that would be real TDD. After that disclaimer let's try to improve a little bit our test. Because maybe we can get to the conclusion that just get in something not null or undefined is not a strong enough test to be secure we have deployed a contract, so let add another test adding a new line.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Glass", function () {
it("Should return our contract", async function () {
const GlassContract = await ethers.getContractFactory("Glass");
const glassContract = await GlassContract.deploy(21000000);
await glassContract.deployed();
expect(glassContract).to.exist;
expect(ethers.utils.isAddress(glassContract.address)).to.be.true;
});
});
What we are testing now is that our glassContract have an address, so with this I think we are a bit closer to be secure we have a contract in our hands.
But let's modify a little bit our code because right now we are doing two test inside or our it function, so let's split it like this.
it("Should return our contract", async function () {
const GlassContract = await ethers.getContractFactory("Glass");
const glassContract = await GlassContract.deploy(21000000);
await glassContract.deployed();
expect(glassContract).to.exist;
});
it("Should has an address", async function () {
const GlassContract = await ethers.getContractFactory("Glass");
const glassContract = await GlassContract.deploy(21000000);
await glassContract.deployed();
expect(ethers.utils.isAddress(glassContract.address)).to.be.true;
});
Notice how verbose are our tests just so close to natural language and that's perfectly find because this test are also be meant to be read in order to understand what our program (smart-contract in this case) does. But there is something is bothering me, let's refactor our code a little bit. Refactoring by definition is the process of restructuring computer code without changing or adding to its external behavior and functionality.
In this case what will do here is extract that part were we repeat our deployment (not a bad thing, we want a contract fresh for each test we take) and put in inside a function that will do exactly that without have the necessity of duplicate our code.
describe("Glass", function () {
let glassContract;
beforeEach(async function () {
const GlassContract = await ethers.getContractFactory("Glass");
glassContract = await GlassContract.deploy(21000000);
await glassContract.deployed();
});
it("Should return our contract", async function () {
expect(glassContract).to.exist;
});
it("Should has an address", async function () { expect(ethers.utils.isAddress(glassContract.address)).to.be.true;
});
});
Like you can see first we declare our glassContract variable to make it available in the scope of all functions inside describe and then we use beforeEach function, a special function that executes before each test. So what we did here is we found that when we added a second test we notice we will need to repeat code everytime we create a new test so in order to avoid that we enclosed our repeated code inside a function that way if we need to modify that code we only need to modify the implementation inside that function. That's in a basic level what we try to do with this refactor process make our code more flexible and ready for new funcionalities.
So let's wrap a little bit this second part of the series.We learn to create test for our contract using javascript language. We learn how to launch our test. And finally we dedicate some time to explain the inevitable phase of refactor our code in order to make it ready to new needs.
In the next article we will finally deploy our contract in a live net so we'll see how to prepare hardhat to do that. As always I hope these series result as useful to you as they are to me.
Top comments (0)