Skip to content

Latest commit

 

History

History
326 lines (269 loc) · 14.6 KB

12.md

File metadata and controls

326 lines (269 loc) · 14.6 KB
title actions requireLogin material
僵尸战斗
checkAnswer
hints
true
editor
language startingCode answer
javascript
test/CryptoZombies.js test/helpers/utils.js test/helpers/time.js
const CryptoZombies = artifacts.require("CryptoZombies"); const utils = require("./helpers/utils"); const time = require("./helpers/time"); const zombieNames = ["Zombie 1", "Zombie 2"]; contract("CryptoZombies", (accounts) => { let [alice, bob] = accounts; let contractInstance; beforeEach(async () => { contractInstance = await CryptoZombies.new(); }); it("should be able to create a new zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); assert.equal(result.receipt.status, true); assert.equal(result.logs[0].args.name,zombieNames[0]); }) it("should not allow two zombies", async () => { await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice})); }) context("with the single-step transfer scenario", async () => { it("should transfer a zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.transferFrom(alice, bob, zombieId, {from: alice}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner, bob); }) }) context("with the two-step transfer scenario", async () => { it("should approve and then transfer a zombie when the approved address calls transferForm", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice, bob, zombieId, {from: bob}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner,bob); }) it("should approve and then transfer a zombie when the owner calls transferForm", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice, bob, zombieId, {from: alice}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner,bob); }) }) it("zombies should be able to attack another zombie", async () => { let result; result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const firstZombieId = result.logs[0].args.zombieId.toNumber(); result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob}); const secondZombieId = result.logs[0].args.zombieId.toNumber(); //TODO: increase the time await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); assert.equal(result.receipt.status, true); }) })
async function shouldThrow(promise) { try { await promise; assert(true); } catch (err) { return; } assert(false, "The contract did not throw."); } module.exports = { shouldThrow, };
async function increase(duration) { //first, let's increase time await web3.currentProvider.sendAsync({ jsonrpc: "2.0", method: "evm_increaseTime", params: [duration], // 86400 seconds in a day id: new Date().getTime() }, () => {}); //next, let's mine a new block web3.currentProvider.send({ jsonrpc: '2.0', method: 'evm_mine', params: [], id: new Date().getTime() }) } const duration = { seconds: function (val) { return val; }, minutes: function (val) { return val * this.seconds(60); }, hours: function (val) { return val * this.minutes(60); }, days: function (val) { return val * this.hours(24); }, } module.exports = { increase, duration, };
const CryptoZombies = artifacts.require("CryptoZombies"); const utils = require("./helpers/utils"); const time = require("./helpers/time"); const zombieNames = ["Zombie 1", "Zombie 2"]; contract("CryptoZombies", (accounts) => { let [alice, bob] = accounts; let contractInstance; beforeEach(async () => { contractInstance = await CryptoZombies.new(); }); it("should be able to create a new zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); assert.equal(result.receipt.status, true); assert.equal(result.logs[0].args.name,zombieNames[0]); }) it("should not allow two zombies", async () => { await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice})); }) context("with the single-step transfer scenario", async () => { it("should transfer a zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.transferFrom(alice, bob, zombieId, {from: alice}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner, bob); }) }) context("with the two-step transfer scenario", async () => { it("should approve and then transfer a zombie when the approved address calls transferForm", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice, bob, zombieId, {from: bob}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner,bob); }) it("should approve and then transfer a zombie when the owner calls transferForm", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const zombieId = result.logs[0].args.zombieId.toNumber(); await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice, bob, zombieId, {from: alice}); const newOwner = await contractInstance.ownerOf(zombieId); assert.equal(newOwner,bob); }) }) it("zombies should be able to attack another zombie", async () => { let result; result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); const firstZombieId = result.logs[0].args.zombieId.toNumber(); result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob}); const secondZombieId = result.logs[0].args.zombieId.toNumber(); await time.increase(time.duration.days(1)); await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); assert.equal(result.receipt.status, true); }) })

哇哦!前几章的信息量有点大哦,但很多都是基础内容。

所以,全部的场景都讲完了吗?还没有哦,压轴的部分肯定会放在最后啦。

我们创建了一个僵尸游戏,那么最精彩的部分是僵尸之间的战斗,对吧?

这个测试非常简单,包括以下步骤:

  • 第一步,我们将创建两个新僵尸 —— 一个 Alice 的,一个 Bob 的。
  • 第二步,Alice 将以 Bob 的 zombieId 作为参数在她的僵尸上运行 attack
  • 最后,为了使测试通过,我们将检查 result.receipt.status 是否等于 true

假设我已经快速编写了所有这些逻辑,将其封装在一个 it() 函数中,并运行了 truffle test 测试。

然后,输出会像这样:

Contract: CryptoZombies
    ✓ should be able to create a new zombie (102ms)
    ✓ should not allow two zombies (321ms)
    ✓ should return the correct owner (333ms)
    1) zombies should be able to attack another zombie
    with the single-step transfer scenario
      ✓ should transfer a zombie (307ms)
    with the two-step transfer scenario
      ✓ should approve and then transfer a zombie when the approved address calls transferFrom (357ms)


  5 passing (7s)
  1 failing

  1) Contract: CryptoZombies
       zombies should be able to attack another zombie:
     Error: Returned error: VM Exception while processing transaction: revert

哦,测试失败了 ☹️

为什么呢?

要搞清楚是怎么回事。首先,我们来仔细看看 createRandomZombie() 背后的代码:

function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

目前为止都没问题。继续,看下 _createZombie()

function _createZombie(string _name, uint _dna) internal {
  uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
  zombieToOwner[id] = msg.sender;
  ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
  emit NewZombie(id, _name, _dna);
}

看到问题了吗?

测试失败的原因是因为我们在游戏中增加了一个冷却时间,使得僵尸在攻击(或进食)后必须等待1天才能再次攻击。

没有这个的话,僵尸每天可以无数次攻击和增殖,这将让游戏很弱智。。

现在我们该怎么办呢…… 等一天吗?

时间旅行

幸好,我们不必等那么久。事实上,根本就不需要等。因为 Ganache 提供了一种通过两个辅助功能及时前行的方法:

  • evm_increaseTime: 增加下一个区块的时间。
  • evm_mine: 挖一个新区块.

你甚至不需要 Tardis 或 DeLorean 来进行这种时间旅行。

让我来解释下这些函数是如何运行的:

  • 每次挖一个新区块时,矿工都会向它添加一个时间戳。假设在第5个区块中挖到了生成僵尸的事务。

  • 接下来,我们调用 evm_increaseTime,但由于区块链是不可变的,所以不可能修改现有区块。所以,当合约检查时间时,它不会增加。

  • 如果我们运行 evm_mine,那么第6个区块就会被挖出(并加上时间戳),这意味着,当我们让僵尸投入战斗时,智能合约将“看到”一天已经过去了。

综上所述,我们可以通过时间旅行来修正我们的测试,具体以下:

await web3.currentProvider.sendAsync({
  jsonrpc: "2.0",
  method: "evm_increaseTime",
  params: [86400],  // there are 86400 seconds in a day
  id: new Date().getTime()
}, () => { });

web3.currentProvider.send({
    jsonrpc: '2.0',
    method: 'evm_mine',
    params: [],
    id: new Date().getTime()
});

嗯,这段代码不错,但是我们不会将这个逻辑添加到我们的 CryptoZombies.js 文件中。

我们已经将所有内容移动到一个名为 helpers/time.js 的新文件中了。要增加时间,只需调用:time.increaseTime(86400);

嗯,还不够完美。毕竟,鬼才知道一天有多少秒呢。

所以我们添加了另一个名为 days辅助函数,它以希望增加时间的天数作为参数。你可以这样来调用这个函数:await time.increase(time.duration.days(1))

注意:很明显,时间旅行在主网或任何由矿工保护的现有测试链上都是不可用的。如果有人可以改变现实世界中时间的运作方式,那岂不就乱套了!对于测试智能合约,时间旅行可是程序员相当重要的一项技能。

实战演习

我们继续填充了那版失败的测试。

  1. 往下滚动,看下我们给你的留言。接下来,如上所示,通过运行 await time.increase 来修复测试用例。。

准备就绪。开始运行 truffle test

Contract: CryptoZombies
    ✓ should be able to create a new zombie (119ms)
    ✓ should not allow two zombies (112ms)
    ✓ should return the correct owner (109ms)
    ✓ zombies should be able to attack another zombie (475ms)
    with the single-step transfer scenario
      ✓ should transfer a zombie (235ms)
    with the two-step transfer scenario
      ✓ should approve and then transfer a zombie when the owner calls transferForm (181ms)
      ✓ should approve and then transfer a zombie when the approved address calls transferForm (152ms)

好了!这就是本章最后一步。