ERC721 合约开发与 Remix 单元测试实战

·

在上一篇中我们搭建了一个可 Mint 的最小可运行 NFT 合约,本篇文章将进一步完善合约功能、优化代码结构,并手把手带你完成 Remix 本地单元测试以太坊主网测试网部署前的全部准备。关键词:Remix 合约测试、Solidity 单元测试、ERC721 铸造方法、NFT 合约示例、eth 支付、JavaScript 测试框架。


继续升级合约

六个关键改动

  1. 将构造函数形参 initialOwner 移除,使用部署者地址直接作为合约 owner;后续部署无需额外输入。
  2. 新增私有变量 _nextTokenId(类型 uint256),记录下一个将被铸造的 NFT ID,避免重复。
  3. 重新定义 mint(uint256 quantity),暂时限制每调用仅铸造 1 枚。
  4. 删除 onlyOwner 修饰符,允许任何用户自由铸造。
  5. 添加 payable 修饰符,铸造需支付 0.01 ether 作为费用,体验真实经济模型。
  6. 使用 _mint 替换 _safeMint,移除 to 形参,改为 msg.sender,降低 Remix IDE 内部调用警告。

代码差异对比

 // SPDX-License-Identifier: MIT
 import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
 import "@openzeppelin/contracts/access/Ownable.sol";

 contract MyToken is ERC721, Ownable {
+    uint256 private _nextTokenId = 0;

-    constructor(address initialOwner)
+    constructor() ERC721("MyToken", "MTK") Ownable(msg.sender) {}

-    function safeMint(address to, uint256 tokenId) public onlyOwner {
+    function mint(uint256 quantity) public payable {
+        require(quantity == 1, "quantity must be 1");
+        require(msg.value == 0.01 ether, "must pay 0.01 ether");
+        uint256 tokenId = _nextTokenId++;
-        _safeMint(to, tokenId);
+        _mint(msg.sender, tokenId);
     }
 }

提醒

  • private:仅合约内部可读写
  • public:链上任意地址可读、合约内可写
    这样的区分能帮助你在复杂业务中快速定位变量作用域。

拓展建议:如果想开放批量 Mint(quantity>1),可把 _nextTokenId 改为 for 循环,并增加 gas 上限检查,以防 DoS。


使用 Remix 单元测试插件

1. 激活插件

打开 Remix 左侧面板底部的「Plugin Manager」,搜索关键字 unit 激活 SOLIDITY UNIT TESTING 插件;图标将固定在左侧导航栏。

👉 掌握这一步,可视化 Solidity 自动测试轻松启动!

2. 理解四个钩子函数

Remix 提供与传统 JavaScript 测试框架类似的钩子函数:

利用这些钩子,可把合约部署、资金初始化、状态重置等逻辑统一管理,测试代码更简洁。

3. 查看自动生成的测试模板

若使用 Remix NFT 项目模板,tests/MyToken_test.sol 会被提前创建。如为空白工作区,点击 Generate 按钮即可迅速生成测试文件,零门槛上手。


编写 & 运行 Solidity 单元测试

引入与初始化

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
import "remix_tests.sol";
import "remix_accounts.sol";
import "../contracts/MyToken.sol";

contract MyTokenTest {
    MyToken s;               // 待测试合约实例
    address acc0;            // 快速获取测试账户

    function beforeAll () public {
        s = new MyToken();
        acc0 = TestsAccounts.getAccount(0);
    }
}

测试 1:校验 name & symbol

function testTokenNameAndSymbol () public {
    Assert.equal(s.name(),   "MyToken", "token name did not match");
    Assert.equal(s.symbol(), "MTK",     "token symbol did not match");
}

测试 2:校验 Mint 逻辑

这里是重点:为防止调用失败,测试函数需附加 #value: 10000000000000000(10¹⁶ wei = 0.01 ether)。

/// #value: 10000000000000000
function testMint() public payable {
    uint256 balanceBefore = s.balanceOf(address(this));
    s.mint{value: msg.value}(1);
    uint256 balanceAfter  = s.balanceOf(address(this));
    Assert.equal(balanceAfter - balanceBefore, 1, "balance mismatch after mint");
}
调试技巧:如果使用 Remix VM,可点击「Debug」按钮查看 call stackmemorystorage 变化,深入理解 payble 工作机制。

执行测试:
选择文件 → 点击 Run → Remix 会启动独立测试链环境,运行结束弹出绿色 ✓ 代表通过。若出现红 ✗,根据失败提示快速定位代码或测试流程问题即可。


FAQ

Q1:为什么 Mint 方法改用 _mint 后不再报 “ERC721: transfer to non ERC721Receiver” 警告?
A1:_safeMint 会检测接收地址是否实现 IERC721Receiver 接口;Remix 测试时收件地址是合约自身,未实现接口容易触发回退。改用 _mint 移除安全检查即可绕过,注意在生产环境需要自行验证收件地址合法性。

Q2:每次 Mint 都设置固定 0.01 ether 合理吗?如果想做分级定价怎么办?
A2:当然可以通过 require(msg.value >= price * quantity, "Need more ETH") + mapping 分级来实现,但务必在合约层面控制变量精度(如用 wei 参与运算)以防精度误差。

Q3:Solidity 测试与 JavaScript(Chai+Mocha)测试如何选择?
A3:Solidity 单元测试运行更快、易于覆盖高频核心业务;JavaScript 测试则更适合在复杂 DApp 场景中结合前端模拟用户交互。最佳实践为 双轨并行:合约用 Solidity 做单元,前端用 JS 做集成。

Q4:Remix VM 和真实测试网有什么关系?
A4:Remix VM 是本地内存级别的链,不会把数据真正写到主网;部署到 Sepolia、Mumbai 等测试网时,须切换 Injected Web3 环境并准备相应水龙头 ETH / MATIC。

Q5:除了 Remix,还有哪些 IDE 支持一键跑 Solidity 单元测试?
A5:Hardhat、Foundry、Truffle 均支持自动测试并配合 CI 集成。未来可考虑把代码迁移到 Foundry,以享受更高效的 forge test 并行执行特性。


用 JavaScript 进行多链环境集成测试(可选)

在 Remix 工作区新建 scripts/mint.test.js,示例用 Chai + Mocha 语法:

const { expect } = require("chai");
const { ethers }  = require("hardhat");

describe("MyToken", function () {
  it("should mint 1 NFT and change balance", async function () {
    const [owner] = await ethers.getSigners();
    const Token   = await ethers.getContractFactory("MyToken");
    const token   = await Token.deploy();
    await token.deployed();

    await token.mint(1, { value: ethers.utils.parseEther("0.01") });
    expect(await token.balanceOf(owner.address)).to.equal(1);
  });
});

👉 点击深入了解 .sol + .js 测试双剑合璧的完整流程!

完成后,运行 npx hardhat test 或右键 Run 即可看到终端打印绿色 ✓,在 CI 中集成毫无压力。


下一步:编译并部署

至此,代码逻辑已完善,本地测试通过,距离真实链上部署只差「合约编译 + Gas 估算 + 网络选择」。
如果你准备将 NFT 部署到以太坊 Sepolia、Polygon Amoy 或 BSC Testnet,请预留:

祝你一路绿灯——下篇我们会分享使用 Ant Design Web3 组件 + React 的前端交互,做到页面一键连接钱包 & 立即 Mint,敬请期待!