- Guide Requirements
- Useful JS + Solidity Testing Resources
- Step 1: Hardhat Project Structure Setup
- Step 2: Add a Faucet.sol Contract File
- Step 3: Add Test File Structure
- - A lot of the logic in the contract depends on the owner being set correctly in the constructor, so weβll want to test that.
- Step 4. Add Withdrawal Amount Test π
- - We donβt want someone instantly draining all of our funds, so we should check that the require clause in the withdraw() function works as expected
- Step 5 - Challenge: Add Critical Function Tests β’οΈ
- - The destroyFaucet() function should only be called by the contract owner, as should the withdrawAll function.
- Learn More About Ethereum Development
How to Unit Test a Smart Contract
To unit test a Solidity smart contract using Hardhat, set up a project structure, add a Faucet.sol contract file, and create a test file structure. Use describe and it functions to define the test suite and targets. Test withdraw() , destroyFaucet() , and withdrawAll() functions.
To unit test a Solidity smart contract using Hardhat, set up a project structure, add a Faucet.sol contract file, and create a test file structure. Use describe and it functions to define the test suite and targets. Test withdraw(), destroyFaucet(), and withdrawAll() functions.
In this guide, weβll cover the fundamentals of using Hardhat to unit test a Solidity smart contract. Testing is one of the most important parts of smart contract development, so letβs jump right in! π¦
We will be setting up some simple tests on a Faucet.sol smart contract while covering some of the different aspects of Solidity testing using JavaScript.
Guide Requirements
- Hardhat: Hardhat is an Ethereum development platform that provides all the tools needed to build, debug and deploy smart contracts.
Useful JS + Solidity Testing Resources
We will use these resources throughout this guide but bookmark these for any other testing you do!
Step 1: Hardhat Project Structure Setup
- In a directory of your choice, run
npm init -y - Run
npm install --save-dev hardhat - Run
npx hardhatand you will get the following UI on your terminal:

- Select
Create a JavaScript project
You will then get a few more options such as if you want to create a .gitignore and install some dependencies like in the following image:

- Select YES to all of these options!
It might take a minute or two to install everything! πΏ
Your project should now contain the following:
- Files:
node_modules,package.json,hardhat.config.js,package-lock.json,README.md - Folders:
scripts,contracts,test
Step 2: Add a Faucet.sol Contract File
- In your
/contractsdirectory, go ahead and delete theLock.solthat Hardhat includes for you by default
You can do this by running rm -rf Lock.sol in your terminal or just delete it manually via your IDE
- Run
touch Faucet.sol - Open the file and copy-paste the following:
- Save the file. πΎ
- Check out / audit the contract! π β¬οΈ
- Start thinking about what we could possibly test for! π€ Lots of things right? Letβs list out a few:
- A lot of the logic in the contract depends on the owner being set correctly in the constructor, so weβll want to test that.
- We donβt want someone instantly draining all of our funds, so we should check that the
requireclause in thewithdraw()function works as expected - The
destroyFaucet()function should only be called by the owner, as should thewithdrawAllfunction.
Letβs set up some unit tests to test that all of these assumptions are correct! π§ͺ
Step 3: Add Test File Structure
We will build out our unit tests for our Faucet.sol. As we build out the test script, we will cover some of the important parts of Solidity testing.
- In your
/testfolder, rename the sample file included by Hardhat either fromLock.jstofaucetTests.js - You are welcome to create your own test file in this folder from scratch. Hardhat already gives us a pre-written scaffold in
Lock.jsso better to take advantage of that and just re-name the sample file - Woah, this sample file has a TON of stuff! π€― Those are just tests relevant to the sample
Lock.jsfile included by Hardhat, letβs clean the file and repurpose for theFaucet.solcontract - Open the
faucetTests.jsfile and copy-paste the following:
Letβs first define some of these newer terms like describe and itβ¦ π
In the code above, we open a describe function called Faucet. The best way to think of this is just a general function scope that βdescribesβ the suite of test cases enumerated by the βitβ functions inside.
Inside that describe, we have an it function. These are your specific unit test targetsβ¦ just sound it out!: βI want it to x.β, βI want it to y.β, etc.
Inside the it function, we use the loadFixture functionality we imported in the first line to help bring all the variables we need for each test easily.
Inside the deployContractAndSetVariables function, we use the contractFactory abstraction provided to us by Ethers.
From the Hardhat testing docs: A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, so Faucet here is a factory for instances of our faucet contract.
We then await for the faucet instance we created from our ContractFactory to be deployed. This is our basic setup - after all these lines, we now have a deployed contract instance with which we can test! We then return them via loadFixture so that we can use them super easily via:
The code is ready to test as soon as you save it. It includes just one simple unit test checking that owner is set correctly at contract deployment. β
It is basically testing the following line in the Faucet.sol constructor:
If you want to see how we wrote the code above, check this video out!! π β¬οΈ
- Run
npx hardhat testin your terminal - if you successfully set up all of the above, you should see:

Weβve successfully accounted for the first assumption we made above π:
- A lot of the logic in the contract depends on the owner being set correctly in the constructor, so weβll want to test that.
Step 4. Add Withdrawal Amount Test π
Letβs continue working through each assumption we made above. Next one is:
- We donβt want someone instantly draining all of our funds, so we should check that the require clause in the withdraw() function works as expected
Do you think you can do this by yourself? Take a moment and try to think how you would implement this testβ¦
Hint: Itβs basically adding a new
it()block! π§βπ»
Letβs run through adding a new unit test for this assumptionβ¦ π
- Add a new
itfunction scope
Pro-tip: just copy-paste the entire previous
itfunction and replace the contents for the new test! No need to write out the whole syntax again. Like this:

- As shown in the gif above, name it something that denotes we are testing the withdraw functionality of the contract
For now, we want to test that we canβt withdraw more than .1 ETH as denoted by the require statement in our contractβs withdraw() function.
Itβs time to use expect! again
Since we want to use expect, weβll need to import some special functionality more specific to Solidity. We will be using these Solidity Chai Matchers.
This import is already in the file! π―
ethereum-waffleshould already be installed, but runnpm install ethereum-wafflejust in case
Cool, we have the necessary imports and installations. π§©
As opposed to the first unit test, we will the Revert Chai Matcher to expect a transaction to revert. This is how we make sure we cover certain cases that we expect should revert.
- Add the following variable to your
deployContractAndSetVariablesfunction:
-
Remember to
returnit:return { faucet, owner, withdrawAmount }; -
Add the following to your newly created
itblock:
We are creating withdrawAmount variable equal to 1 ether, which is way over what the require statement in the withdraw() function allows; so we expect it to revert! π«
Go ahead and change the value to be less than .1 ETH and see the terminal get angry when you run npx hardhat testβ¦ not reverting! π±
- Our test file should look like this so far:
Run npx hardhat test, do your tests pass? π€¨ If so, heck yeahhhhh! π
Step 5 - Challenge: Add Critical Function Tests β’οΈ
We have just one more initial assumption to test:
- The destroyFaucet() function should only be called by the contract owner, as should the withdrawAll function.
This last one shouldnβt be too bad to test! We just need to make sure the onlyOwner modifier is working, similar to the first test. These are some of the most important (in fact, critical!!) functions in our contract so we want to make sure they are indeed only callable by the owner.
As a challenge, implement these tests! Some good corner cases to test with these two functions:
- can only the owner call them?
- does the contract actually self-destruct when the
destroyFaucet()is called? (this one is tricky! hint:getCode) - does the
withdrawAll()function successfully return all of the ether held in the smart contract to the caller?
Use the same testing flow outlined above for efficiency! Here is the suggested flow:
- Just copy-paste a current
itblock - Replace with whatever new functionality you need specific to your new testing assumption
- Remember to update any necessary variables in the
deployContractAndSetVariablesfunction andreturnthem - Import the variables into your
itblock via:
There are many more cases that you can test for to create really iron-clad and comprehensive unit tests - and thus create iron-clad smart contracts! πͺ The testing rabbit hole is particularly great for anyone looking to get a solid foundation in smart contract security, lots of testing there for sure! Good luck, smart contract tester! π«‘
Learn More About Ethereum Development
Alchemy University offers free web3 development bootcamps that explain how to test smart contracts and help developers master the fundamentals of web3 technology. Sign up for free, and start building today!