跳转至主要内容

如何在测试中模拟 Solidity 智能合约

solidity智能合同测试模拟
中级
Markus Waas
soliditydeveloper.com(opens in a new tab)
2020年5月2日
6 分钟阅读 minute read

模拟对象(opens in a new tab)是面向对象编程中的一种常见设计模式。 Mock 一词来源于古法语词“mocquer”,意为“嘲笑,取笑”,但它渐渐拥有了“模拟真实事物”的含义,这实际上也是我们在编程时所做的事情。 不要随意拿你的智能合约开玩笑,但一定要尽可能多地模拟它们。 这将使你的工作更轻松。

使用模拟合约对合约进行单元测试

对合约进行模拟,本质上是创建一个与该合约行为类似的副本,但开发者可以轻易控制这个副本。 通常你会拥有复杂的合约,而你只想对合约其中一小部分进行单元测试。 问题在于,如果测试这一小部分需要合约进入一个非常特别但又难以进入的状态,会怎样?

可以每次都编写将合约带入所需状态的复杂测试设置逻辑,也可以写一个模拟合约。 利用继承,对合约进行模拟比较容易。 只需创建继承原始合约的另一个模拟合约即可。 这时,你就可以重写模拟合约中的函数。 让我们通过一个例子来理解它。

示例:私密 ERC20 合约

本文使用一个在开始时提供私密时间的示例 ERC-20 合约。 合约所有者可以管理私密用户,而且只有这些用户才能在开始时接收代币。 经过特定一段时间后,所有人就都可以使用代币了。 如果你感到好奇,我们使用新 OpenZeppelin 合约(第三版)中的 _beforeTokenTransfer(opens in a new tab) 钩子进一步阐释。

1pragma solidity ^0.6.0;
2
3import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4import "@openzeppelin/contracts/access/Ownable.sol";
5
6contract PrivateERC20 is ERC20, Ownable {
7 mapping (address => bool) public isPrivateUser;
8 uint256 private publicAfterTime;
9
10 constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public {
11 publicAfterTime = now + privateERC20timeInSec;
12 }
13
14 function addUser(address user) external onlyOwner {
15 isPrivateUser[user] = true;
16 }
17
18 function isPublic() public view returns (bool) {
19 return now >= publicAfterTime;
20 }
21
22 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
23 super._beforeTokenTransfer(from, to, amount);
24
25 require(_validRecipient(to), "PrivateERC20: invalid recipient");
26 }
27
28 function _validRecipient(address to) private view returns (bool) {
29 if (isPublic()) {
30 return true;
31 }
32
33 return isPrivateUser[to];
34 }
35}
显示全部
复制

现在我们对此合约进行模拟。

1pragma solidity ^0.6.0;
2import "../PrivateERC20.sol";
3
4contract PrivateERC20Mock is PrivateERC20 {
5 bool isPublicConfig;
6
7 constructor() public PrivateERC20(0) {}
8
9 function setIsPublic(bool isPublic) external {
10 isPublicConfig = isPublic;
11 }
12
13 function isPublic() public view returns (bool) {
14 return isPublicConfig;
15 }
16}
显示全部
复制

你将得到以下错误消息之一:

  • PrivateERC20Mock.sol: 类型错误:重写函数缺少"override"修饰符。(TypeError: Overriding function is missing "override" specifier.)
  • PrivateERC20.sol:类型错误:尝试重写非virtual函数。 您是否忘记了添加“virtual”关键字?(TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?.)

由于使用的是新的 Solidity 0.6 版本,所以必须为可被重写的函数添加 virtual 关键字,为执行重写的函数添加 override。 因此,我们为两个 isPublic 函数添加这些关键字。

现在,在单元测试中,你就可以使用 PrivateERC20Mock 了。 想要在私密使用期间测试合约的行为时,请使用 setIsPublic(false);同样,可以使用 setIsPublic(true) 在公共使用期间测试。 当然,在本例中,我们也可以只使用时间帮助器(opens in a new tab)相应地修改时间。 但至此,模拟合约的概念应该已经清楚了,并且你也可以想象一下不仅仅需要修改时间的复杂场景。

对多个合约进行模拟

如果每进行一次模拟都要创建另一个合约,那将是一件很麻烦的事。 如果你被这种情况困扰,可以考虑 MockContract(opens in a new tab) 库。 它允许用户实时重写和更改合约的行为。 但是,该库只能用来模拟对另一个合约的调用,因此对于上面的示例并不适用。

模拟技术还可以更强大

模拟技术的强大之处远不仅于此。

  • 增加函数:不只是重写某个特定函数的功能很有用,额外增加函数的功能也有其用武之地。 对于代币来说,一个很好的示例是增加 mint 函数,让任何用户都可以免费获得新代币。
  • 在测试网上使用:当你在测试网上部署和测试你的合约以及去中心化应用程序时,请考虑使用模拟合约。 如非必须,请尽量避免重写函数。 毕竟你想要测试真实的逻辑。 然而,举例来说,添加重置函数是有用的,它只是将合约状态重置为初始状态,而无需再部署一个新合约。 显然,你不想在主网合约中添加这样的函数。

上次修改时间: @TTa77(opens in a new tab), 2023年8月15日

本教程对你有帮助吗?