In Part 1, we designed and implemented the smart contract for Dappcord, our Web3-powered Discord alternative.
Now, before deploying it to the blockchain, we need to test it rigorously in a development environment to ensure it functions correctly.
✔ Setting up development environment
✔ Writing and executing automated tests for our contract
✔ Using Ethers.js to interact with smart contracts
✔ Testing different scenarios, including contract deployment, channel creation, user interactions, and fund withdrawals
In this guide, we’ll walk through setting up your local development environment to work with Dappcord, a Web3-based decentralized application. We’ll cover cloning the repository, installing dependencies, and running basic tests to ensure everything is working properly.
Before we get started, make sure you have the following installed on your system:
- Node.js (v16 or later) — Download from nodejs.org
- npm or yarn — Comes with Node.js
- Git — Install from git-scm.com
- Hardhat — A development framework for Ethereum smart contracts
- Metamask — A browser extension wallet
First, navigate to the directory where you want to store the project and clone the starter_code branch:
cd ~/your-workspacerm -rf dappcord # Remove any existing directory to avoid conflicts
git clone https://github.com/dappuniversity/dappcord --branch starter_code
Navigate into the project directory and install all required dependencies:
cd dappcordnpm install
This will install Hardhat and other necessary packages.
Ensure Hardhat is installed and working properly:
npx hardhat --versionIf you see a version number, Hardhat is successfully installed. If not, try installing it manually:
npm install --save-dev hardhatBefore deploying or testing, compile your smart contracts to check for errors:
npx hardhat compileYou should see a success message like:
Compiled 1 Solidity file successfullyTo ensure everything is set up correctly, run the test suite:
npx hardhat testIf everything is working, you should see test results printed on your terminal.
You’re now ready to start building on Dappcord! This setup ensures that your environment is properly configured for smart contract development. Stay tuned for more tutorials on deploying and interacting with smart contracts.
First, create a new project directory and initialize it:
mkdir dappcordcd dappcord
npm init -y
Now, install the required dependencies:
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethersNext, initialize Hardhat in your project:
npx hardhatMany of the warnings are about deprecated packages. While they’re not breaking issues, it’s good practice to replace them with newer alternatives. Some of these warnings come from dependencies that Hardhat and other libraries use internally, so you might not be able to remove them all.
To ensure you’re using the latest compatible versions of key packages, update Hardhat and its dependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify ethersAfter everything is installed, verify Hardhat is working:
npx hardhat --versionIf it works, you’re good to go! 🎯
To ensure everything runs smoothly, restart your terminal and try running Hardhat commands again, like:
npx hardhat compileSelect “Create a basic sample project” when prompted. This will generate a basic Hardhat setup with essential configurations.
We need to automate contract testing to ensure everything works before deploying.
Let’s create a comprehensive test suite in the test directory. Create a file named Dappcord.test.js:
// Import testing librariesconst { expect } = require("chai")
const { ethers } = require("hardhat")
// Helper function to convert to ether units
const tokens = (n) => {
return ethers.utils.parseUnits(n.toString(), 'ether')
}
We’re using:
- chai for assertions - a popular assertion library that makes it easy to write expressive tests
- ethers.js for interacting with the Ethereum blockchain
- A helper function tokens() to convert regular numbers to wei (the smallest unit of Ether)
describe("Dappcord", function () {
// Declare variables used across test cases
let dappcord, deployer, user
const NAME = "Dappcord"
const SYMBOL = "DC"
// Set up fresh contract instance before each test
beforeEach(async () => {
// Setup accounts - deployer is admin, user is regular member
[deployer, user] = await ethers.getSigners()
// Deploy contract with constructor parameters
const Dappcord = await ethers.getContractFactory("Dappcord")
dappcord = await Dappcord.deploy(NAME, SYMBOL)
// Create initial "general" channel for testing
const transaction = await dappcord.connect(deployer).createChannel("general", tokens(1))
await transaction.wait()
})
In the beforeEach block, which runs before each test:
- We set up two accounts: deployer (the contract owner) and user (a regular user)
- We deploy a fresh instance of our Dappcord contract
- We create a “general” channel costing 1 ETH
This ensures each test starts with the same clean state, making our tests isolated and predictable.
describe("Deployment", function () {it("Sets the name correctly", async () => {
// Fetch name from contract
let result = await dappcord.name()
// Verify name matches expected value
expect(result).to.equal(NAME)
})
it("Sets the symbol correctly", async () => {
// Fetch symbol from contract
let result = await dappcord.symbol()
// Verify symbol matches expected value
expect(result).to.equal(SYMBOL)
})
it("Sets the owner to deployer address", async () => {
// Fetch owner from contract
const result = await dappcord.owner()
// Verify owner is the deployer account
expect(result).to.equal(deployer.address)
})
})
- Ensures the contract name and symbol are correctly set
- Confirms the deployer is set as the owner
We now test whether an admin can successfully create chat channels.
describe("Creating Channels", () => {it('Increments total channels counter', async () => {
// Check if totalChannels is updated after channel creation
const result = await dappcord.totalChannels()
expect(result).to.be.equal(1)
})
it('Stores channel attributes correctly', async () => {
// Retrieve channel data using getChannel function
const channel = await dappcord.getChannel(1)
// Verify all channel properties match expected values
expect(channel.id).to.be.equal(1)
expect(channel.name).to.be.equal("general")
expect(channel.cost).to.be.equal(tokens(1))
})
})
- Confirms totalChannels increases after creating a channel
- Ensures the channel’s name, cost, and ID are stored correctly
Now, let’s test if users can join a channel by paying the required ETH.
describe("Joining Channels", () => {const ID = 1
const AMOUNT = ethers.utils.parseUnits("1", "ether")
// Set up channel join operation before each test in this group
beforeEach(async () => {
// User joins channel by minting NFT and paying fee
const transaction = await dappcord.connect(user).mint(ID, { value: AMOUNT })
await transaction.wait()
})
// Check if hasJoined mapping is updated for user
const result = await dappcord.hasJoined(ID, user.address)
expect(result).to.be.equal(true)
})
it('Increases NFT total supply', async () => {
// Verify totalSupply counter increments after mint
const result = await dappcord.totalSupply()
expect(result).to.be.equal(ID)
})
it('Updates contract balance with payment', async () => {
// Confirm contract balance increases by payment amount
const result = await ethers.provider.getBalance(dappcord.address)
expect(result).to.be.equal(AMOUNT)
})
})
- Confirms that users can join channels
- Ensures NFT supply increases when a user mints
- Checks ETH payment updates contract balance
Finally, let’s test if the admin can withdraw funds collected from users.
describe("Withdrawing Funds", () => {const ID = 1
const AMOUNT = ethers.utils.parseUnits("10", 'ether')
let balanceBefore
// Set up withdrawal scenario before each test
beforeEach(async () => {
// Record owner balance before transaction
balanceBefore = await ethers.provider.getBalance(deployer.address)
// User joins channel by paying fee
let transaction = await dappcord.connect(user).mint(ID, { value: AMOUNT })
await transaction.wait()
// Owner withdraws collected fees
transaction = await dappcord.connect(deployer).withdraw()
await transaction.wait()
})
// Compare owner balance after withdrawal
const balanceAfter = await ethers.provider.getBalance(deployer.address)
// Balance should increase (exact amount will be less due to gas fees)
expect(balanceAfter).to.be.greaterThan(balanceBefore)
})
it('Resets contract balance to zero', async () => {
// Verify contract balance is emptied after withdrawal
const result = await ethers.provider.getBalance(dappcord.address)
expect(result).to.equal(0)
})
})
})
- Confirms the contract balance resets after withdrawal
- Ensures the owner receives the withdrawn funds
Now, let’s execute our test suite:
npx hardhat testIf all tests pass, you should see:
DappcordDeployment
✓ Sets the name correctly
✓ Sets the symbol correctly
✓ Sets the owner to deployer
Creating Channels
✓ Increments total channels
✓ Stores channel attributes correctly
Joining Channels
✓ Marks user as joined
✓ Increases total NFT supply
✓ Updates contract balance
Withdrawing Funds
✓ Increases owner balance
✓ Resets contract balance
10 passing (1s)
🎉 Congratulations! Your smart contract is now fully tested and ready for deployment.
Now that our smart contract is built and tested, the next steps are:
1.)Deploy the smart contract on a local Hardhat network
2.)Develop the frontend using React & Ethers.js
3.)Connect users’ Web3 wallets (MetaMask integration)
4.)Create a UI for joining and browsing channels
5.)Integrate real-time chat using Socket.io
In Part 3, we’ll first deploy our smart contract locally on Hardhat, then move on to building the React frontend with Web3 wallet support!
Have questions? Drop a comment!
Follow me for more Web3 tutorials!
Special Thanks to Dapp University for the inspiration!