依賴於真實時間來測試以太坊智慧合約是非常困難。在本教程中,我們將詳細介紹構建測試基礎架構,以使用Solidity,Ganache和Truffle支援智慧合約的時間敏感測試。
我們將探討與時間相關的程式碼測試以及可能不為人所知的方法和工具。這些方法可以幫助開發人員社羣編寫更強大的合同和更多的單元測試,希望我們能夠共同避免對DAO和Parity Wallet Bug進行類似的攻擊。
測試軟體版本:
1、Truffle v5.0.5 (core: 5.0.5)
2、Solidity v0.5.0 (solc-js)
3、Node v11.10.0
4、Ganache CLI v6.3.0 (ganache-core: 2.4.0)
時間設定
要介紹的第一個功能是在Ganache CLI中設定啟動時間,該功能於2018年10月新增,是6.2.0版本的一部分。
ganache-cli — help
…
-t, — timeDate (ISO 8601) that the firstblock should start. Use this feature, along with the
evm_increaseTime method totesttime-dependent code. [string]
…
示例(設定時間)
啟動客戶端,日期設定為2019年2月15日15:53:00 UTC。
$ ganache-cli --time ‘2019-02-15T15:53:00+00:00’
在另一個裝置中啟動控制檯,它將自動與客戶端連線。
$ truffle console
truffle(development)> blockNum = await web3.eth.getBlockNumber()
undefined
truffle(development)> blockNum
0
truffle(development)> block = await web3.eth.getBlock(blockNum)
undefined
truffle(development)> block[‘timestamp’]
1550245980
將unix時間戳轉換為日期字串。
$ date -u -r “1550245980”
Fri Feb 1515:53:00 UTC 2019
日期時間戳是2019年2月15日 - 使用--time標誌傳遞到ganache-cli的日期。
示例(不要設定時間)
在終端中啟動客戶端,但這次不設定時間。
$ ganache-cli
在另一個終端中啟動控制檯,它將自動與客戶端連線。
$ truffle console
truffle(development)> blockNum = await web3.eth.getBlockNumber()
undefined
truffle(development)> blockNum
0
truffle(development)> block = await web3.eth.getBlock(blockNum)
undefined
truffle(development)> block[‘timestamp’]
1552252405
將unix時間戳轉換為日期字串。
$ date -u -r “1552252405”
Sun Mar 1021:13:25 UTC 2019
日期時間戳是建立示例的日期:2019年3月10日。
示例合約和程式碼測試
透過固定起始塊時間,可以編寫在特定時間之前和之後執行功能的測試。下面是一個示例TimeContract以及用於演示此操作的測試。
pragma solidity ^0.5.0;
contract TimeContract {
uint256 private startTime;
constructor(uint256 newStartTime) public {
startTime = newStartTime;
}
/**
* isNowAfter will return true if now is after the given start time
*/
functionisNowAfter() externalviewreturns (bool){
return (now >= startTime);
}
}
TimeContract.sol
const TimeContract = artifacts.require('./TimeContract');
const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;
contract('TimeContract', async (accounts) => {
before('deploy TimeContract', async() => {
instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
});
it("Sun Feb 10 00:00:00 UTC 2019 (before current time)", async() => {
var output = await instance_1.isNowAfter.call();
assert.equal(output, true, "output should have been true");
});
it("Wed Mar 20 00:00:00 UTC 2019 (after current time)", async() => {
var output = await instance_2.isNowAfter.call();
assert.equal(output, false, "output should have been false");
});
});
TestTimeContract.js
第一個測試斷言當前時間是在2019年2月10日,即2019 00:00:00 UTC之後。
第二次測試斷言當前時間是在2019年3月20日星期三00:00:00 UTC之後。
測試結果
透過將當前時間設定為2019年2月15日星期五15:53:00 UTC來啟動ganache客戶端。
$ ganache-cli --time2019-02-15T15:53:00+00:00
以下是測試的輸出:
$ truffle test
Compiling ./contracts/Migrations.sol…
Compiling ./contracts/TimeContract.sol…
Contract: TimeContract
✓ Sun Feb 1000:00:00 UTC 2019 (before current time)
✓ Wed Mar 2000:00:00 UTC 2019 (after current time)
2 passing (181ms)
在這裡可以看出,上面的斷言確實是正確的。
跳躍時間
討論的第二個特徵是跳躍時間。
ganache EVM中有JSON RPC API方法,可以模擬時間事件:
方法
+-----------------------------+-----------------------------+
| Action | Ganache EVM Name |
+-----------------------------+-----------------------------+
| Advance the time | evm_increaseTime |
| Advance the block(s) | evm_mine |
| Advance both time and block | evm_increaseTime + evm_mine |
| Save time | evm_snapshot |
| Revert time | evm_revert |
+-----------------------------+-----------------------------+
看一下下面的程式碼片段,它包含測試幫助器方法的上述方法:
advanceTime = (time) => {
returnnew Promise((resolve, reject) => {
web3.currentProvider.send({
jsonrpc: '2.0',
method: 'evm_increaseTime',
params: [time],
id: new Date().getTime()
}, (err, result) => {
if (err) { return reject(err) }
return resolve(result)
})
})
}
advanceBlock = () => {
returnnew Promise((resolve, reject) => {
web3.currentProvider.send({
jsonrpc: '2.0',
method: 'evm_mine',
id: new Date().getTime()
}, (err, result) => {
if (err) { return reject(err) }
const newBlockHash = web3.eth.getBlock('latest').hash
return resolve(newBlockHash)
})
})
}
takeSnapshot = () => {
returnnew 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 = (id) => {
returnnew 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)
})
})
}
advanceTimeAndBlock = async (time) => {
await advanceTime(time)
await advanceBlock()
return Promise.resolve(web3.eth.getBlock('latest'))
}
module.exports = {
advanceTime,
advanceBlock,
advanceTimeAndBlock,
takeSnapshot,
revertToSnapShot
}
utils.js
該片段是Andy Watt的程式碼擴充套件,來自本文開頭提到的,增加了兩個新方法。
請呼叫advanceTimeAndBlock(<time>)或advanceTime(<time>),以及要前進的秒數(<time>)。這些方法使用JSON RPC evm_increaseTime和evm_mine方法。
例如,要向前移動10天,請按以下方式使用utils.js檔案:
//86400 seconds in a day
advancement = 86400 * 10// 10 Days
await helper.advanceTimeAndBlock(advancement)
以下是更新的測試程式碼:
const TimeContract = artifacts.require('./TimeContract');
const helper = require('./utils/utils.js');
const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;
const SECONDS_IN_DAY = 86400;
contract('TimeContract', async (accounts) => {
before('deploy TimeContract', async() => {
instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
});
it("Sun Feb 10 00:00:00 UTC 2019 (before current time)", async() => {
var output = await instance_1.isNowAfter.call();
assert.equal(output, true, "output should have been true");
});
it("Wed Mar 20 00:00:00 UTC 2019 (after current time)", async() => {
var output = await instance_2.isNowAfter.call();
assert.equal(output, false, "output should have been false");
});
it("Wed Mar 20 00:00:00 UTC 2019 (after current time)", async() => {
await helper.advanceTimeAndBlock(SECONDS_IN_DAY * 100); //advance 100 days
var output = await instance_2.isNowAfter.call();
assert.equal(output, true, "output should have been true");
});
});
TestTimeContract.js
在這裡,最後一次測試將時間向前移動並斷言'now'是在instance_2的日期之後。
這是實際應用的測試:
$ truffle test
Compiling ./contracts/Migrations.sol…
Compiling ./contracts/TimeContract.sol…
Contract: TimeContract
✓ Sun Feb 1000:00:00 UTC 2019 (before current time) (39ms)
✓ Wed Mar 2000:00:00 UTC 2019 (after current time)
✓ Wed Mar 2000:00:00 UTC 2019 (after current time) (48ms)
3 passing (249ms)
可以看出斷言是正確的。
但是,如果再次執行測試,則其中一個測試失敗:
$ truffle test
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/TimeContract.sol...
Contract: TimeContract
✓ Sun Feb 1000:00:00 UTC 2019 (before current time)
1) Wed Mar 2000:00:00 UTC 2019 (after current time)
> No events were emitted
✓ Wed Mar 2000:00:00 UTC 2019 (after current time) (40ms)
2 passing (237ms)
1 failing
1) Contract: TimeContract
Wed Mar 2000:00:00 UTC 2019 (after current time):
output should have been false
+ expected - actual
-true
+false
at Context.it (TestTimeContract.js:22:16)
at processTicksAndRejections (internal/process/next_tick.js:81:5)
這是為什麼?
在之前的測試執行中,時間向前移動了100天。本地區塊鏈及時記錄了進展情況,並從我們的開始日期起保持在+100天。因此,下次執行測試時,它們將失敗,因為第二次測試執行的開始日期將從上一次測試執行的結束日期開始。為了使這些測試再次成功執行,需要使用--time標誌重新啟動Ganache CLI,其初始日期如上所述:2019-02-15t15:53:00+00:00。
向後跳
當向後跳時,首先要捕捉當前時間,以便知道向後跳到哪裡。TakeSnapshot()和RevertToSnapshot(ID)方法就是這樣做的。這些方法使用json rpc evm_snapshot和evm_revert方法。
例如,要獲取快照並捕獲ID,請使用utils.js檔案,方法如下:
snapShot = await helper.takeSnapshot()
snapshotId = snapShot[‘result’]
要使用捕獲的ID還原快照,請使用:
await helper.revertToSnapShot(snapshotId)
以下是更新的測試程式碼:
const TimeContract = artifacts.require('./TimeContract');
const helper = require('./utils/utils.js');
const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;
const SECONDS_IN_DAY = 86400;
contract('TimeContract', async (accounts) => {
before('deploy TimeContract', async() => {
instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
});
beforeEach(async() => {
snapShot = await helper.takeSnapshot();
snapshotId = snapShot['result'];
});
afterEach(async() => {
await helper.revertToSnapShot(snapshotId);
});
it("Sun Feb 10 00:00:00 UTC 2019 (before current time)", async() => {
var output = await instance_1.isNowAfter.call();
assert.equal(output, true, "output should have been true");
});
it("Wed Mar 20 00:00:00 UTC 2019 (after current time)", async() => {
var output = await instance_2.isNowAfter.call();
assert.equal(output, false, "output should have been false");
});
it("Wed Mar 20 00:00:00 UTC 2019 (after current time)", async() => {
await helper.advanceTimeAndBlock(SECONDS_IN_DAY * 100); //advance 100 days
var output = await instance_2.isNowAfter.call();
assert.equal(output, true, "output should have been true");
});
});
TestTimeContract.js
這可確保在執行任何測試之前儲存ID。在每次測試之後,使用ID呼叫revert以返回到執行測試之前的狀態。
輸出與“Jumping Forward”中的輸出沒有區別,但測試可以反覆執行而不會失敗。
在檢視ganache-cli的輸出時,請注意EVM命令,因為它儲存,向前移動,並在時間上向後移動。
evm_snapshot
Saved snapshot #2
evm_increaseTime
evm_mine
eth_getBlockByNumber
eth_getBlockByNumber
eth_getBlockByNumber
net_version
eth_call
evm_revert
Reverting to snapshot #2
結論
透過引入操作時間的功能,可以單獨測試依賴於時間的程式碼。引入無狀態性確保了測試執行之間的等冪性,從而使區塊鏈的測試開發變得更加容易。希望這裡的演示有用,並使社羣能夠編寫更安全、更可靠的合同。如果您有任何問題或意見,請在下面分享;歡迎合作,並鼓勵流動性。