当你在以太坊上搭建一个工资类型的Dapp时,编写精确的时间依赖性测试是必要的。
在开始之前,你需要搭建的环境如下:
-
您的测试框架是Truffle、Ganache+Mocha。
-
测试尽可能精确(<5秒误差范围)。
在为以太坊智能合约编写的测试中操纵时间会带来一些问题。
陷阱#1:RPC
虽然在Ganache中完全可以把时间向前和向后跳跃,但有多种方法可以实现这一目标。要获得准确的结果,必须按以下方式使用evm_mine RPC方法:
{
"jsonrpc": "2.0",
"method": "evm_mine",
"params": ["NUMBER_OF_SECONDS"],
"id": 1
}
advanceBlockAtTime.json
也作为javascript的许可:
const advanceBlockAtTime = (time) => {
return new Promise((resolve, reject) => {
web3.currentProvider.send(
{
jsonrpc: "2.0",
method: "evm_mine",
params: [time],
id: new Date().getTime(),
},
(err, _) => {
if (err) {
return reject(err);
}
const newBlockHash = web3.eth.getBlock("latest").hash;
return resolve(newBlockHash);
},
);
});
};
advanceBlockAtTime.js
笔记:
-
如果没有NUMBER_OF_SECONDS参数,则RPC调用仅增加块高度,但不会及时跳转。
-
“id”参数是可选的,但很好。你在测试时把什么值放进去并不重要。还有evm_increaseTime,它增加了Ganache的“内部时钟”,这样无论何时挖掘下一个区块,它都有一个时间戳偏移量。这增加了开销:
// Not what you want
advanceTimeAndBlock = async (time) => {
await advanceTime(time)
await advanceBlock()
return Promise.resolve(web3.eth.getBlock('latest'))
}
advanceTimeAndBlock.js
没错,你必须进行两次RPC调用,而第一种方法只需要一次。
Jakub Wojciechowski提出道具,为ganache-core提供PR#13。如果没有确定性和原子性的方法在Ganache中及时跳跃,那么编写准确的测试将会很困难。
陷阱#2:运行时间(run time)
代码本身需要时间来执行。具体来说,Javascript许可需要花费不可忽略的时间来解决问题。
这很明显,对吧?当为以太坊智能合约编写依赖时间的测试时,事情变得很微妙。
考虑以下内容:
-
测试用例运行所需的时间
-
您希望将来跳转的秒数
-
在控制台中提交yarn run test时的unix时间戳
取决于这些变量,您的测试块可能会在一秒或多秒的过程中被捕获,因此您的断言可能会中断。
例如:
describe("when the stream did start but not end", function() {
beforeEach(async function() {
await advanceBlockAtTime(
now
.plus(STANDARD_TIME_OFFSET)
.plus(5)
.toNumber(),
);
});
describe("when the withdrawal amount is within the available balance", function() {
const amount = new BigNumber(5).multipliedBy(1e18).toString(10);
it("makes the withdrawal", async function() {
const balance = await this.token.balanceOf(recipient);
await this.sablier.withdraw(streamId, amount, opts);
const newBalance = await this.token.balanceOf(recipient);
balance.should.be.bignumber.equal(newBalance.minus(amount));
});
});
});
makeWithdrawal.js
这样做是因为它要求Sablier合同撤回以前存入的余额。ERC-1620中规定调用者可以退出多少的规则。
我在区块之前记录了mocha的unix时间戳,我测量了使用节点的性能时序api运行测试所需的时间:
t0 1565455128964
Call to sablier.withdraw took 115.92673601210117 milliseconds.
1) makes the withdrawal
sablierWithdrawTest.txt
如果您将115加上156545128964,则最终得到一个以9079结尾的数字,因此秒数从8增加到9。这就是打破这个断言的原因,因为我期望X的余额,当我实际得到X+1时。(超过秒数=更多钱)
虽然编写一个可以计算另一个程序P2完成所需时间的程序P1是不可能的(参见图灵的暂停问题),但我们可以安全地假设你的beforeEach和它之间的阻塞时间不应超过1秒。这假设您的节点实例和ganache之间的来回通信几乎是即时的,即使在运行覆盖时也是如此。
这是修复:
balance.should.bignumber.satisfy(function(num) {
return (
num.isEqualTo(newBalance.minus(amount)) || num.isEqualTo(newBalance.minus(amount).plus(ONE_UNIT))
);
});
makeWithdrawalFix.js
其中ONE_UNIT是每秒分配的一个货币单位,根据Sablier模型。它不完美,但比使用“大于”或“小于”平等检查更好。
最后,正如OpenZeppelin团队在此论证的那样,您可能不需要这种精确度。如果你的dapp不直接涉及时间戳或区块数,那么容忍更大的时间偏移是完全正常的。
陷阱#3:BeforeEach和AfterEach
您的里程可能会有所不同,但您可能希望在“beforeEach”中向前跳跃并在“afterEach”中向后跳跃。这是因为您的合同可能在“describe”块的范围内定义了一些变量,并且您希望运行一系列“it”块,这些块都采用相同的状态。不回复“afterEach”只会永远增加时间戳。
例如:
describe("when the stream did start but not end", function() {
const amount = new BigNumber(5).multipliedBy(1e18).toString(10);
beforeEach(async function() {
await web3.utils.advanceBlockAtTime(
now
.plus(STANDARD_TIME_OFFSET)
.plus(5)
.toNumber(),
);
});
it("test1", function() {});
it("test2", function() {});
it("test3", function() {});
afterEach(async function() {
await web3.utils.advanceBlockAtTime(now.toNumber());
});
});
beforeAfterEach.js
正如您在上面的代码片段中看到的,我们有三个测试,其中我们假设提取的金额为5。在Sabilier的上下文中,在时间15秒内前进将产生15秒的可提取量,因此我们必须在“aftereach”块中返回到原始状态。
陷阱#4:快照
在完成所有测试后返回到原始状态。它可能对CI或其他外部环境有帮助。
takeSnapshot = async () => {
return new Promise((resolve, reject) => {
web3.currentProvider.send(
{
jsonrpc: "2.0",
method: "evm_snapshot",
id: new Date().getTime(),
},
(err, snapshotId) => {
if (err) {
return reject(err);
}
return resolve(snapshotId);
},
);
});
};
revertToSnapshot = async (id) => {
return new Promise((resolve, reject) => {
web3.currentProvider.send(
{
jsonrpc: "2.0",
method: "evm_revert",
params: [id],
id: new Date().getTime(),
},
(err, result) => {
if (err) {
return reject(err);
}
return resolve(result);
},
);
});
};
snapshotFunctions.js
在代码库中定义这些函数,然后将其插入到一个根测试文件中:
let snapshot;
let snapshotId;
before(async () => {
snapshot = await takeSnapshot();
snapshotId = snapshot.result;
});
after(async () => {
await revertToSnapshot(snapshotId);
});
truffleSnapshots.js
现在你的区块链将在所有神奇的时间跳跃后恢复到原来的时间戳。
本文中使用的一些零碎内容受到启发或从其他着作中获取,例如Ethan Wessel令人惊叹的“使用truffle测试时间”。 该文章唯一的警告是使用evm_mine和evm_increaseTime,我们在上面解释了为什么这不理想。
此外,这是一个非常好的StackExchange线程,它具有block.timestamp和一些GitHub线程的固有安全性,它们揭示了在Ganache(1和2)中确定性时间跳跃的历史。