Membangun lapisan kepercayaan kontrak: unit test dengan Hardhat dan Foundry, fuzzing untuk mencari bug tak terduga, serta invariant testing untuk menjamin properti kontrak selalu benar

Setelah di episode 6 kalian menguasai toolchain, pada episode ini kita membangun lapisan yang menentukan apakah kontrak kalian bisa dipercaya: testing. Di dunia web tradisional, bug berarti error page. Di blockchain, bug berarti dana hilang — dan tidak bisa dibatalkan.
Kontrak production 2026 tidak lolos review tanpa tiga lapis test: unit test (kasus spesifik), fuzzing (input acak), dan invariant testing (properti yang selalu benar). Episode ini melatih ketiganya.
Unit test menulis ekspektasi untuk kasus spesifik. Contoh test ERC-20:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
let token, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
const Factory = await ethers.getContractFactory("MyToken");
token = await Factory.deploy(1_000_000);
await token.waitForDeployment();
});
it("menetapkan totalSupply ke pemilik", async function () {
expect(await token.totalSupply()).to.equal(ethers.parseEther("1000000"));
expect(await token.balanceOf(owner.address)).to.equal(
await token.totalSupply()
);
});
it("memindahkan token dan mengeluarkan event", async function () {
const amount = ethers.parseEther("10");
await expect(token.transfer(user.address, amount))
.to.emit(token, "Transfer")
.withArgs(owner.address, user.address, amount);
expect(await token.balanceOf(user.address)).to.equal(amount);
});
it("menolak transfer saat saldo tidak cukup", async function () {
await expect(
token.connect(user).transfer(owner.address, 1)
).to.be.revertedWith("insufficient balance");
});
});Jalankan dengan npx hardhat test. Perhatikan tiga pola: happy path, event verification, dan negative case. Test tanpa negative case adalah test yang belum selesai.
Foundry menulis test dalam Solidity — lebih dekat ke kontrak dan sangat cepat:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
function setUp() public {
token = new MyToken(1_000_000);
}
function testTransfer() public {
token.transfer(address(0x123), 100);
assertEq(token.balanceOf(address(0x123)), 100);
}
function testRevertWhenInsufficient() public {
vm.expectRevert("insufficient balance");
token.transfer(address(0x123), type(uint256).max);
}
}forge test -vvvFuzzing memanggil fungsi dengan ribuan input acak. Sering kali bug terletak pada nilai ekstrem yang tidak pernah terpikirkan: nol, nilai maksimum, atau type(uint256).max yang menyebabkan overflow.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract FuzzTest is Test {
MyToken token;
function setUp() public {
token = new MyToken(1_000_000);
}
// Property: saldo pengguna tidak pernah bertambah di luar totalSupply
function testFuzzTotalSupplyInvariant(uint256 amount) public {
token.approve(address(this), type(uint256).max);
vm.assume(amount <= token.balanceOf(address(this)));
vm.assume(token.balanceOf(address(this)) >= amount);
token.transfer(address(0xabc), amount);
uint256 sum = token.balanceOf(address(this)) +
token.balanceOf(address(0xabc)) +
token.balanceOf(address(0));
assertLe(sum, token.totalSupply());
}
}Foundry menjalankan ribuan kasus otomatis dan melaporkan counterexample jika ada property yang gagal. Hardhat memiliki tool serupa (@nomicfoundation/hardhat-chai-matchers + waffle), tapi kecepatan fuzz Foundry tak tertandingi.
Tip
Gunakan vm.assume() untuk mempersempit domain input fuzz ke nilai yang masuk akal secara logika bisnis. Fuzz tanpa batasan domain akan membuang banyak waktu pada input yang memang harus gagal.
Invariant testing menjalankan serangkaian transaksi acak dan memverifikasi bahwa properti kontrak selalu benar dalam keadaan apa pun. Ini level tertinggi pengujian — dipakai tim audit untuk menganalisis protokol kompleks.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
import {Vault} from "../src/Vault.sol";
contract InvariantTest is Test {
Vault vault;
// Invariant: total ETH di kontrak = total deposit seluruh pengguna
function invariant_totalDepositsMatchBalance() public view {
assertEq(address(vault).balance, vault.totalDeposits());
}
}Jalankan dengan forge test — Foundry mengeksekusi transaksi acak berulang kali dan memeriksa invariant di setiap akhir. Contoh invariant lain yang umum: totalSupply konstan, saldo tidak pernah negatif, dan jumlah shares selalu konsisten.
Test suite production yang baik mengikuti hierarki:
├── Unit test → setiap fungsi, setiap edge case
├── Integration test → interaksi antar kontrak (misal DEX + token)
├── Fork test → test terhadap state mainnet yang di-fork
└── Fuzz + invariant → property yang selalu benarFork test patut diperhatikan: dengan anvil --fork-url https://mainnet..., kalian bisa menguji kontrak kalian terhadap state mainnet nyata (saldo nyata, likuiditas nyata) tanpa biaya — teknik standar untuk membangun integrasi dengan protokol yang sudah ada.
Inti yang harus dibawa pulang:
Di episode 8 selanjutnya kita beranjak dari kontrak ke aplikasi — dApp Development: membangun frontend yang berinteraksi dengan kontrak lewat ethers.js dan viem, menghubungkan wallet pengguna, dan merakit dApp sederhana yang bisa kalian pakai. Mari lanjut ke episode 8!