Testing Your Contracts
Để chạy các test ở trong thư mục test
, ta sử dụng câu lệnh sau:
yarn hardhat test
Nếu muốn chạy test ở một mạng nào đó, ta dùng câu lệnh sau:
yarn hardhat test --network <network name>
Writing Tests
Giả sử ta có contract sau:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Token {
string public name = "My Hardhat Token";
string public symbol = "MHT";
uint256 public totalSupply = 1000000;
address public owner;
mapping(address => uint256) balances;
// The Transfer event helps off-chain applications understand
// what happens within your contract.
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() {
// The totalSupply is assigned to the transaction sender, which is the
// account that is deploying the contract.
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
function transfer(address to, uint256 amount) external {
// Check if the transaction sender has enough tokens.
// If `require`'s first argument evaluates to `false` then the
// transaction will revert.
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
// Notify off-chain applications of the transfer.
emit Transfer(msg.sender, to, amount);
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
Và ta có file Token.ts
ở trong thư mục test
có nội dung như sau:
import { ethers } from "hardhat";
import { expect } from "chai";
describe("Token contract", function () {
it("Deployment should assign the total supply of tokens to the owner", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
Đoạn code trên sử dụng thư viện Mocha và Chai.
Sau đây là một số giải thích:
Mocha
Khi sử dụng Mocha, chúng ta có thể dùng hàm describe
để khai báo test section, với:
- Tham số thứ nhất là tên section.
- Tham số thứ hai là một callback chứa các test cần chạy.
describe("Token contract", function () {})
Hàm it
giúp mô tả test, với:
- Tham số thứ nhất là mô tả của test.
- Tham số thứ hai là một hàm chứa các bước mà test sẽ thực hiện.
it("Deployment should assign the total supply of tokens to the owner", async function () {})
Ethers
Ở đây ta sử dụng đối tượng ethers
của thư viện “hardhat” thay vì từ “ethers” như khi sử dụng Ethers. Mục đích là để chúng ta có thể gọi sử dụng phương thức getSigners
hay getContractFactory
.
import { ethers } from "hardhat";
Phương thức getSigners
được dùng để lấy ra các account có trong network dưới dạng mảng.
const [owner] = await ethers.getSigners();
Phương thức getContractFactory
giúp khởi tạo một instance của contract mà không cần phải biết địa chỉ hay ABI của contract (chỉ cần truyền vào tên của contract).
const Token = await ethers.getContractFactory("Token");
Khi test thì ta cũng cần phải thực hiện deploy, phương thức deploy
sẽ được gọi từ instance của contract.
const hardhatToken = await Token.deploy();
Chai
Mục đích của test này là kiểm tra xem balance của owner (được truy xuất bằng hàm balanceOf
của contract) sau khi contract được deploy có bằng với giá trị totalSupply
đã được đề cập ở trong contract hay không.
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
Hàm expect
nhận vào một giá trị và bao nó trong một Assertion
object. Object này có nhiều utility method dùng để assert giá trị chẳng hạn như equal
1. Ta gọi các utility method này là các matcher.
Thực chất, Hardhat sử dụng plugin Hardhat Chai Matchers bao gồm nhiều matcher mở rộng hỗ trợ cho việc testing.
Using a Different Account
Nếu muốn gọi một phương thức với một account khác không phải là account mặc định, ta có thể sử dụng phương thức connect
thông qua instance của contract như sau:
import { ethers } from "hardhat";
import { expect } from "chai";
describe("Token contract", function () {
// ...previous test...
it("Should transfer tokens between accounts", async function() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
// Transfer 50 tokens from owner to addr1
await hardhatToken.transfer(addr1.address, 50);
expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);
// Transfer 50 tokens from addr1 to addr2
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
});
});
Reusing Common Test Setups with Fixtures
Ở trong hai đoạn test ở trên, ta thấy được rằng có một số đoạn code bị lặp lại (chẳng hạn như việc gọi các phương thức từ ethers
hay deploy contract).
Để tái sử dụng những đoạn code này, ta có thể sử dụng một tính năng của Hardhat có tên là fixture. Về cơ bản, fixture là một hàm chứa các test setup mà chỉ chạy một lần duy nhất khi được gọi. Với những lần gọi sau, nó sẽ reset lại state của network về trạng thái sau lần chạy đầu tiên.
Attention
Chú ý, fixture này chỉ hoạt động với Hardhat Network.
Ta định nghĩa fixture như sau:
import { ethers } from "hardhat";
describe("Token contract", function () {
async function deployTokenFixture() {
// Get the ContractFactory and Signers here.
const Token = await ethers.getContractFactory("Token");
const [owner, addr1, addr2] = await ethers.getSigners();
// To deploy our contract, we just have to call Token.deploy() and await
// its deployed() method, which happens once its transaction has been mined.
const hardhatToken = await Token.deploy();
await hardhatToken.deployed();
// Fixtures can return anything you consider useful for your tests
return { Token, hardhatToken, owner, addr1, addr2 };
}
// ...Tests...
}
Và gọi sử dụng bằng cách truyền vào hàm loadFixture
như sau:
import { ethers } from "hardhat";
import { expect } from "chai";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
describe("Token contract", function () {
// ...Fixture...
it("Should assign the total supply of tokens to the owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
it("Should transfer tokens between accounts", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await expect(
hardhatToken.transfer(addr1.address, 50)
).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
await expect(
hardhatToken.connect(addr1).transfer(addr2.address, 50)
).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
});
}
Matcher changeTokenBalances
giúp kiểm tra số dư token của các tài khoản có bị thay đổi hay không2, bao gồm ba tham số:
- Tham số đầu tiên là instance của contract.
- Tham số thứ hai là danh sách các tài khoản cần kiểm tra.
- Tham số thứ ba là một mảng chứa các thay đổi về số dư của các tài khoản.
Trong test case thứ ba ở trong đoạn code trên, ta mong muốn số dư của tài khoản addr1
sẽ giảm 50 và số dư của tài khoản addr2
sẽ tăng 50 sau khi addr1
chuyển 50 token cho addr2
.