brianfive, piablo @ D_D Academy
September 19, 2022
Twitter @brianfive, @Skruffster
Created by:
Avatarbrianfive, piablo @ D_D Academy
September 19, 2022
Twitter @brianfive, @Skruffster
Welcome to this lesson where we’ll be adding automated tests for our TierNFT smart contract created in the previous tutorial. In case you haven’t completed it already, we have a fast-track guide below to that project set-up - assuming you’re confident with Solidity and a professional development environment. If you're not, we’d recommend completing the tutorials in this track before this one, before moving on to Connecting your TierNFT Smart Contract to a Frontend, our project series finalé.
Back here, you’ll learn about the benefits of writing automated tests, general testing concepts and best practices. You’ll walk away with many practical tests for smart contracts and nibble on some food for thought about test-driven development.
As you dive in to the lesson, you'll come across some thought-provoking checkpoint questions designed to test your existing knowledge, measure how well you're absorbing the fresh content, and we might even ask you to predict next steps in the lesson. And to wrap up, a final quiz awaits. To complete this adventure might take anywhere from one to four hours. That all depends on your previous experience and how much new ground you need to cover. But enjoy the journey and remember to take regular breaks along the way. We care about your well-being and have even included a nifty pomodoro timer ⌛ in the menu header. Let it remind you to appreciate nature's gifts and "go touch some grass" for a while. 🌱 Embrace the process.
Let's raise the sails to catch the wind of what's to come:
And set sail . . .
Testing can:
Tests are a tool for writing your code and making sure it keeps working as expected when changes are made. Automated tests don't fully replace manually testing your smart contract but it's an important tool to ensuring your contract will work as expected.
If you have an existing project from your previous work on the TierNFT lesson, you have all the Hardhat dependencies installed. Otherwise let's create a new Hardhat project and copy in the contract which we'll be testing. To create a new project, please refer to Lesson 3 - Tier NFTs and search for the “First things first 👷♂️" section to get your project going. Follow the steps until "Let’s start coding”, then open up your code editor. If you are using VSCode, type
Add a line in your
"scripts": { "something": "some other script", "test": "hardhat test --network hardhat" },
If you don't already have
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; string constant SVG_START = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">'; string constant SVG_END = "</text></g></svg>"; string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721, Ownable { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) { tierId = 2; } else if (msg.value >= TIER_VALUE_1) { tierId = 1; } totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } // Create the tokenURI json on the fly without creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); string memory tierName = tokenTier[tokenId] == 2 ? TIER_NAME_2 : tokenTier[tokenId] == 1 ? TIER_NAME_1 : TIER_NAME_0; string memory imageSVG = string( abi.encodePacked(SVG_START, tierName, SVG_END) ); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', name(), " #", Strings.toString(tokenId), '", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(imageSVG)), '","attributes":[{"trait_type": "Tier", "value": "', tierName, '" }]}' ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } // Function to withdraw funds from contract function withdraw() public onlyOwner { // Check that we have funds to withdraw uint256 balance = address(this).balance; require(balance > 0, "Balance should be > 0"); // Withdraw funds. (bool success, ) = payable(owner()).call{value: balance}(""); require(success, "Withdraw failed"); } }
One small, but crucial change before we proceed. Ensure the version of Solidity being used to run the contract is the same one configured in
Copy the Solidity version from the
For example, since our contract is using Solidity
require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); module.exports = { solidity: "0.8.12", };
⌛ Pomodoro? 😊 🌱
Now that the tools are set up, let's talk about what we want to test in our smart contract. We'll then create the tests.
What is the contract trying to do?
If any of these functions are broken, you may not be able to mint NFTs, render the correct art or withdraw funds at the end.
We want to test that those functions work in a variety of cases.
For example, we can test that a mint happens when enough Eth is spent and that a mint fails if there is not enough Eth spent.
If you don't have one already, create a
Let's start with testing the
In addition to these first two tests we'll add setup code that we'll reuse for all the other tests too.
Please add the following code to your new
const { expect } = require("chai"); const CONTRACT_NAME = "TierNFT"; const COLLECTION_NAME = "TierNFT"; const COLLECTION_SYMBOL = "Tier"; describe("TierNFT", function () { let contract; let owner; let otherUser; beforeEach(async function () { const Contract = await hre.ethers.getContractFactory(CONTRACT_NAME); const [_owner, _otherUser] = await hre.ethers.getSigners(); owner = _owner; otherUser = _otherUser; contract = await Contract.deploy(COLLECTION_NAME, COLLECTION_SYMBOL); await contract.waitForDeployment(); }); describe("constructor", async () => { it("set proper collection name", async function () { const name = await contract.name(); expect(name).to.equal("TierNFT"); }); it("set proper collection symbol", async function () { const symbol = await contract.symbol(); expect(symbol).to.equal("Tier"); }); }); });
Before we walk through, let's make sure everything is working correctly. Run the tests using
Now let's walk through the test code we've pasted in and explain what it does.
At the top we see constants with the name and symbol of the NFT collection:
const CONTRACT_NAME = "TierNFT"; const COLLECTION_NAME = "TierNFT"; const COLLECTION_SYMBOL = "Tier";
Next is a
When we run the tests we see:
TierNFT constructor ✔ set proper collection name ✔ set proper collection symbol
Notice "TierNFT" comes from the first
Next notice the
What's happening in
The hardhat framework gives us the tools to deploy our contract to a locally-running blockchain. So when the
Those
Finally we then come across our first two actual tests:
it("set proper collection name", async function () { const name = await contract.name(); expect(name).to.equal("TierNFT"); }); it("set proper collection symbol", async function () { const symbol = await contract.symbol(); expect(symbol).to.equal("Tier"); });
They each call a method and verify the results. "Is the contract name really TierNFT?" "Is the contract symbol really Tier?"
You'll notice a lot of
Lastly when you write a test, always make sure it fails first! So try changing the
So after intentionally causing one of our tests to fail, here is what the failure looks like:
TierNFT construction 1) set proper collection name ✔ set proper collection symbol 1 passing (2s) 1 failing 1) TierNFT construction set proper collection name: AssertionError: expected 'TierNFT' to equal 'Anita' + expected - actual -TierNFT +Anita
Now is time to add tests for our
Where exactly do we add this
Currently the end of our
describe('constructor', async () => { it('set proper collection name', async function () { const name = await contract.name() expect(name).to.equal('TierNFT') }) it('set proper collection symbol', async function () { const symbol = await contract.symbol() expect(symbol).to.equal('Tier') }) }) // this is where existing describe section ends })
Our new
Make sure you keep that last line
describe("mint", async () => { it("should not mint if value is below the minimum tier", async function () { await expect( contract.mint({ value: hre.ethers.parseEther("0.001"), }), ).to.be.revertedWith("Not enough value for the minimum Tier"); await expect(await contract.totalSupply()).to.equal(0); }); it("should increase total supply", async function () { await contract.mint({ value: hre.ethers.parseEther("0.01"), }); await expect(await contract.totalSupply()).to.equal(1); }); it("should mint Tier 0", async function () { await contract.mint({ value: hre.ethers.parseEther("0.01"), }); await expect(await contract.tokenTier(1)).to.equal(0); }); it("should mint Tier 1", async function () { await contract.mint({ value: hre.ethers.parseEther("0.02"), }); await expect(await contract.tokenTier(1)).to.equal(1); }); it("should mint Tier 2", async function () { await contract.mint({ value: hre.ethers.parseEther("0.05"), }); await expect(await contract.tokenTier(1)).to.equal(2); }); });
These tests introduce additional code that are helpful when testing contracts.
With minting, one needs to pay the right price, we should also see the total supply of TierNFTs increase with each mint, and since we're minting 3 different tiers, we need to test each one of those gets minted based on the right price.
The first test is a great example of how to test to ensure an expected error occurs. If not enough Eth is sent, the contract fails with an error and the transaction is reverted as if nothing happened.
In this first test we want to ensure our method call is properly reverted when the proper amount of Eth is not provided. This happens here:
await expect( contract.mint({ value: hre.ethers.parseEther("0.001"), }), ).to.be.revertedWith("Not enough value for the minimum Tier");
We're expecting the call to be reverted when not sending enough payment, along with a specific error message from the contract.
Here we're just calling
Now for the remaining mint tests, these all depend on a successful mint, so they all need to mint with a minimum of
And each of those remaining tests has a single
Notice each line tests the first token (
Let's look at testing
describe("withdrawal", async () => { it("should error if not owner", async function () { await expect(contract.connect(otherUser).withdraw()).to.be.revertedWith( "Ownable: caller is not the owner", ); }); it("should error if balance is zero", async function () { await expect(contract.withdraw()).to.be.revertedWith("Balance should be > 0"); }); it("should success if owner", async function () { await contract.mint({ value: hre.ethers.parseEther("0.01"), }); expect(await hre.ethers.provider.getBalance(contract.getAddress())).to.equal( hre.ethers.parseEther("0.01"), ); await contract.withdraw(); expect(await hre.ethers.provider.getBalance(contract.getAddress())).to.equal( hre.ethers.parseEther("0"), ); }); });
You'll see similar patterns from the previous
⌛ Pomodoro? 😊 🌱
Here's where things get even more interesting!
Notice the current version of
This is impossible to test because if you make a change to the code, you get a different Base64 string. But how do you know if it's correct?
You need to refactor this method into smaller helper methods, each of which are more testable.
But first let's use the power of testing to make sure we don't break the implementation as we refactor. Let's wrap
Here's an example of what that looks like:
it("should return base64-encoded data for tokenURI for Tier 0", async function () { await contract.mint({ value: hre.ethers.parseEther("0.01"), }); await expect(await contract.tokenURI(1)).to.equal( "data:application/json;base64,eyJuYW1lIjogIlRpZXJORlQgIzEiLCAiZGVzY3JpcHRpb24iOiAiVGllck5GVHMgY29sbGVjdGlvbiIsICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTFNREFpSUdobGFXZG9kRDBpTlRBd0lpQm1hV3hzUFNKdWIyNWxJaUJtYjI1MExXWmhiV2xzZVQwaWMyRnVjeTF6WlhKcFppSStQR1JsWm5NK1BHWnBiSFJsY2lCcFpEMGlRU0lnWTI5c2IzSXRhVzUwWlhKd2IyeGhkR2x2YmkxbWFXeDBaWEp6UFNKelVrZENJaUJtYVd4MFpYSlZibWwwY3owaWRYTmxjbE53WVdObFQyNVZjMlVpSUdobGFXZG9kRDBpTlRBd0lpQjNhV1IwYUQwaU5UQXdJajQ4Wm1WRWNtOXdVMmhoWkc5M0lHUjRQU0l4SWlCa2VUMGlNaUlnYzNSa1JHVjJhV0YwYVc5dVBTSTRJaUJtYkc5dlpDMXZjR0ZqYVhSNVBTSXVOamNpSUhkcFpIUm9QU0l5TURBbElpQm9aV2xuYUhROUlqSXdNQ1VpSUM4K1BDOW1hV3gwWlhJK1BHeHBibVZoY2tkeVlXUnBaVzUwSUdsa1BTSkNJaUI0TVQwaU1DSWdlVEU5SWpBaUlIZ3lQU0l4TlRBd01DSWdlVEk5SWpBaUlHZHlZV1JwWlc1MFZXNXBkSE05SW5WelpYSlRjR0ZqWlU5dVZYTmxJajQ4YzNSdmNDQnZabVp6WlhROUlpNHdOU0lnYzNSdmNDMWpiMnh2Y2owaUkyRmtNREJtWmlJZ0x6NDhjM1J2Y0NCdlptWnpaWFE5SWk0eU15SWdjM1J2Y0MxamIyeHZjajBpSXpSbE1EQmxZeUlnTHo0OGMzUnZjQ0J2Wm1aelpYUTlJaTQwTVNJZ2MzUnZjQzFqYjJ4dmNqMGlJMlptTURCbU5TSWdMejQ4YzNSdmNDQnZabVp6WlhROUlpNDFPU0lnYzNSdmNDMWpiMnh2Y2owaUkyVXdaVEJsTUNJZ0x6NDhjM1J2Y0NCdlptWnpaWFE5SWk0M055SWdjM1J2Y0MxamIyeHZjajBpSTJabVpEZ3hNQ0lnTHo0OGMzUnZjQ0J2Wm1aelpYUTlJaTQ1TlNJZ2MzUnZjQzFqYjJ4dmNqMGlJMkZrTURCbVppSWdMejQ4TDJ4cGJtVmhja2R5WVdScFpXNTBQanhzYVc1bFlYSkhjbUZrYVdWdWRDQnBaRDBpUXlJZ2VERTlJakFpSUhreFBTSTJNQ0lnZURJOUlqQWlJSGt5UFNJeE1UQWlJR2R5WVdScFpXNTBWVzVwZEhNOUluVnpaWEpUY0dGalpVOXVWWE5sSWo0OGMzUnZjQ0J6ZEc5d0xXTnZiRzl5UFNJalpEQTBNR0k0SWlBdlBqeHpkRzl3SUc5bVpuTmxkRDBpTVNJZ2MzUnZjQzFqYjJ4dmNqMGlJMlV3WlRCbE1DSWdMejQ4TDJ4cGJtVmhja2R5WVdScFpXNTBQand2WkdWbWN6NDhjR0YwYUNCbWFXeHNQU0oxY213b0kwSXBJaUJrUFNKTk1DQXdhREUxTURBd2RqVXdNRWd3ZWlJK1BHRnVhVzFoZEdWVWNtRnVjMlp2Y20wZ1lYUjBjbWxpZFhSbFRtRnRaVDBpZEhKaGJuTm1iM0p0SWlCaGRIUnlhV0oxZEdWVWVYQmxQU0pZVFV3aUlIUjVjR1U5SW5SeVlXNXpiR0YwWlNJZ1puSnZiVDBpTUNBd0lpQjBiejBpTFRFME5UQXdJREFpSUdSMWNqMGlNVFp6SWlCeVpYQmxZWFJEYjNWdWREMGlhVzVrWldacGJtbDBaU0lnTHo0OEwzQmhkR2crUEdOcGNtTnNaU0JtYVd4c1BTSWpNV1F4WlRJd0lpQmplRDBpTVRBd0lpQmplVDBpT1RBaUlISTlJalExSWlCbWFXeDBaWEk5SW5WeWJDZ2pRU2tpSUM4K1BIUmxlSFFnZUQwaU1UQXhJaUI1UFNJNU9TSWdkR1Y0ZEMxaGJtTm9iM0k5SW0xcFpHUnNaU0lnWTJ4aGMzTTlJbTVtZEV4dloyOGlJR1p2Ym5RdGMybDZaVDBpTXpKd2VDSWdabWxzYkQwaWRYSnNLQ05ES1NJZ1ptbHNkR1Z5UFNKMWNtd29JMEVwSWo1RVgwUThZVzVwYldGMFpWUnlZVzV6Wm05eWJTQmhkSFJ5YVdKMWRHVk9ZVzFsUFNKMGNtRnVjMlp2Y20waUlHRjBkSEpwWW5WMFpWUjVjR1U5SWxoTlRDSWdkSGx3WlQwaWNtOTBZWFJsSWlCbWNtOXRQU0l3SURFd01DQTVNQ0lnZEc4OUlqTTJNQ0F4TURBZ09UQWlJR1IxY2owaU5YTWlJSEpsY0dWaGRFTnZkVzUwUFNKcGJtUmxabWx1YVhSbElpQXZQand2ZEdWNGRENDhaeUJtYjI1MExYTnBlbVU5SWpNeUlpQm1hV3hzUFNJalptWm1JaUJtYVd4MFpYSTlJblZ5YkNnalFTa2lQangwWlhoMElIZzlJakkxTUNJZ2VUMGlNamd3SWlCMFpYaDBMV0Z1WTJodmNqMGliV2xrWkd4bElpQmpiR0Z6Y3owaWRHbGxjazVoYldVaVBrSmhjMmxqUEM5MFpYaDBQand2Wno0OEwzTjJaejQ9IiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ICJUaWVyIiwgInZhbHVlIjogIkJhc2ljIiB9XX0=", ); });
First, here's the new contract that now contains a bunch of new helper methods to make
You'll still see
Let's quickly describe the new helper methods you'll see below. Each one breaks down the "create an encoded SVG" problem into manageable functions that can each be tested and understood by humans writing these tests.
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; string constant SVG_START = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">'; string constant SVG_END = "</text></g></svg>"; string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721, Ownable { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) { tierId = 2; } else if (msg.value >= TIER_VALUE_1) { tierId = 1; } totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } function tierNameOf(uint256 _tokenTier) public pure returns (string memory) { if (_tokenTier == 0) return TIER_NAME_0; if (_tokenTier == 1) return TIER_NAME_1; return TIER_NAME_2; } function imageSVGOf(uint256 _tokenTier) public pure returns (string memory) { return string( abi.encodePacked(SVG_START, tierNameOf(_tokenTier), SVG_END) ); } function finalJSON( string memory _name, uint256 _tokenId, string memory _imageSVG, uint256 _tokenTier ) public pure returns (string memory) { return string( abi.encodePacked( '{"name": "', _name, " #", Strings.toString(_tokenId), '", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,', _imageSVG, '","attributes":[{"trait_type": "Tier", "value": "', tierNameOf(_tokenTier), '" }]}' ) ); } // Create the tokenURI json on the fly without creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); // string memory tierName = tierNameOf(tokenTier[tokenId]); string memory imageSVG = imageSVGOf(tokenTier[tokenId]); string memory json = Base64.encode( bytes( finalJSON( name(), tokenId, Base64.encode(bytes(imageSVG)), tokenTier[tokenId] ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } // Function to withdraw funds from contract function withdraw() public onlyOwner { // Check that we have funds to withdraw uint256 balance = address(this).balance; require(balance > 0, "Balance should be > 0"); // Withdraw funds. (bool success, ) = payable(owner()).call{value: balance}(""); require(success, "Withdraw failed"); } }
Ok, here are some tests for these helpers!
Since an SVG is basically a bunch of strings, you'll notice all these tests are nearly simple single lines -- but they're comparing the results with long strings that eventually make up the final SVG.
describe('tokenURI and helpers', async () => { it('should error if token does not exist', async function () { await expect(contract.tokenURI(0)).to.be.revertedWith('Nonexistent token') }) }) describe('tierNameOf', async function () { it('should return proper tier name for Tier 0', async function () { await expect(await contract.tierNameOf(0)).to.equal('Basic') }) it('should return proper tier name for Tier 1', async function () { await expect(await contract.tierNameOf(1)).to.equal('Medium') }) it('should return proper tier name for Tier 2', async function () { await expect(await contract.tierNameOf(2)).to.equal('Premium') }) it('should return Tier 2 name if tier number greater than all tiers', async function () { await expect(await contract.tierNameOf(999)).to.equal('Premium') }) }) describe('imageSVGOf', async function () { it('should return proper imageSVG for Tier 0', async function () { await expect(await contract.imageSVGOf(0)).to.equal( '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">Basic</text></g></svg>', ) }) it('should return proper imageSVG for Tier 1', async function () { await expect(await contract.imageSVGOf(1)).to.equal( '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">Medium</text></g></svg>', ) }) it('should return proper imageSVG for Tier 2', async function () { await expect(await contract.imageSVGOf(2)).to.equal( '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">Premium</text></g></svg>', ) }) }) describe('finalJSON', async function () { it('should return proper finalJSON for Tier 0', async function () { await expect( await contract.finalJSON('TierNFT', 111, 'IMAGE_SVG_BASE64_HERE', 0), ).to.equal( '{"name": "TierNFT #111", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,IMAGE_SVG_BASE64_HERE","attributes":[{"trait_type": "Tier", "value": "Basic" }]}', ) }) it('should return proper finalJSON for Tier 1', async function () { await expect( await contract.finalJSON('TierNFT', 222, 'IMAGE_SVG_BASE64_HERE', 1), ).to.equal( '{"name": "TierNFT #222", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,IMAGE_SVG_BASE64_HERE","attributes":[{"trait_type": "Tier", "value": "Medium" }]}', ) }) it('should return proper finalJSON for Tier 2', async function () { await expect( await contract.finalJSON('TierNFT', 333, 'IMAGE_SVG_BASE64_HERE', 2), ).to.equal( '{"name": "TierNFT #333", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,IMAGE_SVG_BASE64_HERE","attributes":[{"trait_type": "Tier", "value": "Premium" }]}', ) }) }) it('should return base64-encoded data for tokenURI for Tier 0', async function () { await contract.mint({ value: hre.ethers.parseEther('0.01'), }) await expect(await contract.tokenURI(1)).to.equal( 'data:application/json;base64,eyJuYW1lIjogIlRpZXJORlQgIzEiLCAiZGVzY3JpcHRpb24iOiAiVGllck5GVHMgY29sbGVjdGlvbiIsICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTFNREFpSUdobGFXZG9kRDBpTlRBd0lpQm1hV3hzUFNKdWIyNWxJaUJtYjI1MExXWmhiV2xzZVQwaWMyRnVjeTF6WlhKcFppSStQR1JsWm5NK1BHWnBiSFJsY2lCcFpEMGlRU0lnWTI5c2IzSXRhVzUwWlhKd2IyeGhkR2x2YmkxbWFXeDBaWEp6UFNKelVrZENJaUJtYVd4MFpYSlZibWwwY3owaWRYTmxjbE53WVdObFQyNVZjMlVpSUdobGFXZG9kRDBpTlRBd0lpQjNhV1IwYUQwaU5UQXdJajQ4Wm1WRWNtOXdVMmhoWkc5M0lHUjRQU0l4SWlCa2VUMGlNaUlnYzNSa1JHVjJhV0YwYVc5dVBTSTRJaUJtYkc5dlpDMXZjR0ZqYVhSNVBTSXVOamNpSUhkcFpIUm9QU0l5TURBbElpQm9aV2xuYUhROUlqSXdNQ1VpSUM4K1BDOW1hV3gwWlhJK1BHeHBibVZoY2tkeVlXUnBaVzUwSUdsa1BTSkNJaUI0TVQwaU1DSWdlVEU5SWpBaUlIZ3lQU0l4TlRBd01DSWdlVEk5SWpBaUlHZHlZV1JwWlc1MFZXNXBkSE05SW5WelpYSlRjR0ZqWlU5dVZYTmxJajQ4YzNSdmNDQnZabVp6WlhROUlpNHdOU0lnYzNSdmNDMWpiMnh2Y2owaUkyRmtNREJtWmlJZ0x6NDhjM1J2Y0NCdlptWnpaWFE5SWk0eU15SWdjM1J2Y0MxamIyeHZjajBpSXpSbE1EQmxZeUlnTHo0OGMzUnZjQ0J2Wm1aelpYUTlJaTQwTVNJZ2MzUnZjQzFqYjJ4dmNqMGlJMlptTURCbU5TSWdMejQ4YzNSdmNDQnZabVp6WlhROUlpNDFPU0lnYzNSdmNDMWpiMnh2Y2owaUkyVXdaVEJsTUNJZ0x6NDhjM1J2Y0NCdlptWnpaWFE5SWk0M055SWdjM1J2Y0MxamIyeHZjajBpSTJabVpEZ3hNQ0lnTHo0OGMzUnZjQ0J2Wm1aelpYUTlJaTQ1TlNJZ2MzUnZjQzFqYjJ4dmNqMGlJMkZrTURCbVppSWdMejQ4TDJ4cGJtVmhja2R5WVdScFpXNTBQanhzYVc1bFlYSkhjbUZrYVdWdWRDQnBaRDBpUXlJZ2VERTlJakFpSUhreFBTSTJNQ0lnZURJOUlqQWlJSGt5UFNJeE1UQWlJR2R5WVdScFpXNTBWVzVwZEhNOUluVnpaWEpUY0dGalpVOXVWWE5sSWo0OGMzUnZjQ0J6ZEc5d0xXTnZiRzl5UFNJalpEQTBNR0k0SWlBdlBqeHpkRzl3SUc5bVpuTmxkRDBpTVNJZ2MzUnZjQzFqYjJ4dmNqMGlJMlV3WlRCbE1DSWdMejQ4TDJ4cGJtVmhja2R5WVdScFpXNTBQand2WkdWbWN6NDhjR0YwYUNCbWFXeHNQU0oxY213b0kwSXBJaUJrUFNKTk1DQXdhREUxTURBd2RqVXdNRWd3ZWlJK1BHRnVhVzFoZEdWVWNtRnVjMlp2Y20wZ1lYUjBjbWxpZFhSbFRtRnRaVDBpZEhKaGJuTm1iM0p0SWlCaGRIUnlhV0oxZEdWVWVYQmxQU0pZVFV3aUlIUjVjR1U5SW5SeVlXNXpiR0YwWlNJZ1puSnZiVDBpTUNBd0lpQjBiejBpTFRFME5UQXdJREFpSUdSMWNqMGlNVFp6SWlCeVpYQmxZWFJEYjNWdWREMGlhVzVrWldacGJtbDBaU0lnTHo0OEwzQmhkR2crUEdOcGNtTnNaU0JtYVd4c1BTSWpNV1F4WlRJd0lpQmplRDBpTVRBd0lpQmplVDBpT1RBaUlISTlJalExSWlCbWFXeDBaWEk5SW5WeWJDZ2pRU2tpSUM4K1BIUmxlSFFnZUQwaU1UQXhJaUI1UFNJNU9TSWdkR1Y0ZEMxaGJtTm9iM0k5SW0xcFpHUnNaU0lnWTJ4aGMzTTlJbTVtZEV4dloyOGlJR1p2Ym5RdGMybDZaVDBpTXpKd2VDSWdabWxzYkQwaWRYSnNLQ05ES1NJZ1ptbHNkR1Z5UFNKMWNtd29JMEVwSWo1RVgwUThZVzVwYldGMFpWUnlZVzV6Wm05eWJTQmhkSFJ5YVdKMWRHVk9ZVzFsUFNKMGNtRnVjMlp2Y20waUlHRjBkSEpwWW5WMFpWUjVjR1U5SWxoTlRDSWdkSGx3WlQwaWNtOTBZWFJsSWlCbWNtOXRQU0l3SURFd01DQTVNQ0lnZEc4OUlqTTJNQ0F4TURBZ09UQWlJR1IxY2owaU5YTWlJSEpsY0dWaGRFTnZkVzUwUFNKcGJtUmxabWx1YVhSbElpQXZQand2ZEdWNGRENDhaeUJtYjI1MExYTnBlbVU5SWpNeUlpQm1hV3hzUFNJalptWm1JaUJtYVd4MFpYSTlJblZ5YkNnalFTa2lQangwWlhoMElIZzlJakkxTUNJZ2VUMGlNamd3SWlCMFpYaDBMV0Z1WTJodmNqMGliV2xrWkd4bElpQmpiR0Z6Y3owaWRHbGxjazVoYldVaVBrSmhjMmxqUEM5MFpYaDBQand2Wno0OEwzTjJaejQ9IiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ICJUaWVyIiwgInZhbHVlIjogIkJhc2ljIiB9XX0=', ) }) })
The final test is an example mentioned earlier about adding a test before refactoring with helper methods. It checks that the initial
With all the helpfulness of tests, why didn't we start with them before we wrote any of the contract implementation? The main reason was trying to minimize all the things you needed to learn to get started, but the methodology of writing tests first is called "Test Driven Development" (TDD).
TDD is a very powerful methodology which goes like this:
Voila!
With TDD:
you would have written tests as you go, which are much more fun to write than all at once at the end
you would have ended up writing better code because you discover quickly when code is impossible to test. You'll end up with the added benefit of breaking a big method into multiple smaller methods that are easily testable. So in the refactor example above for
Bolting on tests after the fact can be painful and boring, and you lose the potential code improvements you can make when you test as you go. And speaking of testing as you go, why not take the quiz 😊
You just did some amazing work, and you will do a great service to the web3 ecosystem by being conscious of the value of writing solid code. There’s lots more to explore in Academy, and we’re looking forward to seeing you in our final project in this ERC721 track where you’ll be Connecting your TierNFT Smart Contract to a Frontend. Why not dive into the forum in the mean time, and share your new found wisdom with our community.
Go forth, test, and prosper knowing you have confidence in the code you're deploying into the world.