第7章:保持游戏的乐趣(续) 在本章中,我们将完成第二个测试的代码。它应该执行以下操作:
首先,Alice 应该调用 createRandomZombie 并将 zombieNames[0] 作为她第一个僵尸的名字。
接下来,Alice 应该尝试创建她的第二个僵尸。唯一不同的是,这次僵尸的名字应该设置为 zombieNames[1]。
在这个时候,我们预计合约会抛出一个错误。
由于我们的测试只有在智能合约抛出错误时才算通过,所以我们的逻辑会稍有不同。我们将不得不将第二次调用 createRandomZombie 放在 try/catch 块中,代码如下:
try { await contractInstance.createRandomZombie(zombieNames[1 ], {from : alice}); assert(true ); }catch (err) { return ; } assert(false , "The contract did not throw." );
现在我们得到了我们想要的结果,对吧?
嗯… 我们已经很接近了,但还差一点。
为了保持测试代码的简洁,我们将上面的代码移到了 helpers/utils.js 中,并在 CryptoZombies.js 中导入它,代码如下:
const utils = require ("./helpers/utils" );
然后,调用函数的代码行应该像这样:
await utils.shouldThrow(myAwesomeContractInstance.myAwesomeFunction());
测试一下 在上一章中,我们为第二个测试创建了一个空的框架。现在让我们来填充它。
首先,让 Alice 创建她的第一个僵尸。将 zombieNames[0] 作为名字,并确保正确设置所有者。
在 Alice 创建了第一个僵尸之后,使用 shouldThrow 运行 createRandomZombie,作为参数。如果你不记得如何编写语法,可以参考上面的示例。不过,首先试着自己写写看,不要偷看哦。
太棒了,你刚刚完成了第二个测试的编写!
现在,我们已经为你运行了 truffle test。以下是输出结果:
Contract: CryptoZombies ✓ should be able to create a new zombie (129 ms) ✓ should not allow two zombies (148 ms) 2 passing (1 s)
测试通过了。太棒了!
it("should not allow two zombies" , async () => { await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; await utils.shouldThrow(contractInstance .createRandomZombie (zombieNames [1], {from : alice }) ); })
这段代码是一个 Mocha 测试 ,用于测试智能合约中的 createRandomZombie 函数。我们可以将其分解为以下几个部分来解释:
1. it 函数 it("should not allow two zombies" , async () => { });
it() 是 Mocha 测试框架的一个函数,用来定义一个具体的测试用例。测试用例的描述是 "should not allow two zombies",意味着这个测试的目的是验证合约是否正确限制一个用户不能创建多个僵尸。
async 关键字表明这个测试是异步的,意味着测试中的代码会涉及到与区块链的交互,并且需要等待智能合约的响应。
2. 创建第一个僵尸 await contractInstance.createRandomZombie(zombieNames[0 ], {from : alice});
这行代码调用合约实例 contractInstance 上的 createRandomZombie 方法,并传递一个名字(zombieNames[0])和一个所有者地址(from: alice)。这意味着 Alice 将创建一个名为 zombieNames[0] 的僵尸。
await 用于等待合约调用完成。在这个上下文中,createRandomZombie 可能是一个异步函数,所以我们需要 await 来确保测试在执行后续代码前等待这次调用完成。
3. 验证不能创建第二个僵尸 await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1 ], {from : alice}));
这行代码尝试再次调用 createRandomZombie,并给出一个新的名字(zombieNames[1])。然而,根据合约中的限制,用户只能拥有一个僵尸,所以这次调用应该抛出错误(require(ownerZombieCount[msg.sender] == 0))。
utils.shouldThrow() 是一个辅助函数(位于 helpers/utils.js 中),它的目的是检查给定的合约调用是否抛出了错误。如果抛出错误,说明测试通过;如果没有抛出错误,则测试失败。
这个方法的作用是确保第二次调用 createRandomZombie 时,合约会抛出错误,从而验证我们的逻辑:一个用户不能拥有超过一个僵尸。
总结 这段测试的目的是验证智能合约中的限制:每个用户最多只能拥有一个僵尸。具体流程如下:
Alice 创建了第一个僵尸。
然后,我们验证当 Alice 再次尝试创建第二个僵尸时,合约应该抛出一个错误。
通过 shouldThrow 方法,确保错误被正确抛出,如果抛出错误,测试通过。
这个测试的核心思想是利用 require 来限制用户只能创建一个僵尸,并确保第二次尝试时会触发错误。
第8章:僵尸转移 问题 :假设 Alice 想要将她的僵尸发送给 Bob。我们要测试这个功能吗?
当然要!
如果你一直跟随我们之前的课程,你应该知道,我们的僵尸除了继承自 ERC721 外,还具备 ERC721 的一些基本特性。ERC721 规范提供了两种转移代币的方式:
(1) 第一种方式 function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
第一种方式是 Alice(拥有者)调用 transferFrom,将她的地址作为 _from 参数,Bob 的地址作为 _to 参数,传入她想转移的僵尸 ID。
(2) 第二种方式 function approve(address _approved, uint256 _tokenId) external payable;
接着调用:
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
第二种方式是 Alice 首先调用 approve,将 Bob 的地址和僵尸 ID 作为参数传入,合约记录 Bob 被授权转移该僵尸。接着,当 Alice 或 Bob 调用 transferFrom 时,合约检查 msg.sender 是否为 Alice 或 Bob 的地址。如果是,就将僵尸转移给 Bob。
我们将这两种转移僵尸的方式称为“场景”。为了测试每种场景,我们需要创建两个不同的测试组,并为它们提供有意义的描述。
为什么要分组测试? 我们现在的逻辑比较简单,但将来可能会更复杂。尤其是第二种场景(即 approve 后跟 transferFrom)至少需要两个测试:
首先,我们必须检查 Alice 自己是否能转移僵尸。
其次,我们还必须检查 Bob 是否被允许运行 transferFrom。
此外,将来你可能会添加其他功能,需要不同的测试。我们认为从一开始就设定一个可扩展的结构是最好的做法😉。这样,无论是其他开发者还是你自己,如果在一段时间后重新回顾这些代码,都会更容易理解。
注意 :如果你与其他开发人员一起工作,你会发现他们更有可能遵循你在初期代码中所设定的约定。能够有效协作是你在做大项目时必须掌握的技能。尽早养成这些好的习惯,会让你作为开发者的生活变得更加轻松和成功。
context 函数为了分组测试,Truffle 提供了一个叫做 context 的函数。让我快速展示一下如何使用它来更好地结构化我们的代码:
context("with the single-step transfer scenario" , async () => { it("should transfer a zombie" , async () => { }) }) context("with the two-step transfer scenario" , async () => { it("should approve and then transfer a zombie when the approved address calls transferFrom" , async () => { }) it("should approve and then transfer a zombie when the owner calls transferFrom" , async () => { }) })
如果我们将这些代码添加到 CryptoZombies.js 文件中,并运行 truffle test,输出应该类似如下:
Contract: CryptoZombies ✓ should be able to create a new zombie (100 ms) ✓ should not allow two zombies (251 ms) with the single-step transfer scenario ✓ should transfer a zombie with the two -step transfer scenario ✓ should approve and then transfer a zombie when the owner calls transferFrom ✓ should approve and then transfer a zombie when the approved address calls transferFrom 5 passing (2 s)
但是 ,看一下上面的输出——它看起来好像所有测试都通过了,显然这是不对的,因为我们还没有编写这些测试呢!!
幸运的是,这有一个简单的解决办法——如果我们在 context() 函数前加上 x,就像这样:xcontext(),Truffle 会跳过这些测试。
注意 :你也可以在 it() 函数前加上 x。记住,当你编写完这些函数的测试后,别忘了去掉所有的 x!
现在,运行 truffle test。输出应该类似于下面这样:
Contract: CryptoZombies ✓ should be able to create a new zombie (199 ms) ✓ should not allow two zombies (175 ms) with the single-step transfer scenario - should transfer a zombie with the two -step transfer scenario - should approve and then transfer a zombie when the owner calls transferFrom - should approve and then transfer a zombie when the approved address calls transferFrom 2 passing (827 ms) 3 pending
其中 - 表示已跳过的测试。
是不是很酷?现在你可以在编写测试时标记出你知道需要写测试的空函数,然后继续进行其他工作。
测试一下 赶快复制/粘贴上面的代码。
暂时跳过我们新添加的 context 函数。
我们的测试只是空的框架,还需要写很多逻辑来实现它们。我们将在接下来的章节中分步骤实现这些功能。
答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const CryptoZombies = artifacts.require("CryptoZombies" ); const utils = require("./helpers/utils" ); 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 }) ); }) xcontext("with the single-step transfer scenario" , async () => { it("should transfer a zombie" , async () => { }) }) xcontext("with the two-step transfer scenario" , async () => { it("should approve and then transfer a zombie when the approved address calls transferFrom" , async () => { }) it("should approve and then transfer a zombie when the owner calls transferFrom" , async () => { }) }) })
第9章:ERC721 代币转移 - 单步场景 到目前为止,我们仅仅是做了热身…
但现在是时候真正展示你的知识了!
在接下来的章节中,我们将把所学的内容整合起来,测试一些非常酷的东西。
首先,我们将测试 Alice 将她的 ERC721 代币转移给 Bob 的场景,采用单步操作。
测试应该做什么:
为 Alice 创建一个新的僵尸(记住,僵尸就是一个 ERC721 代币)。
确保 Alice 将她的 ERC721 代币转移给 Bob。
此时,Bob 应该拥有该 ERC721 代币。如果是这样,调用 ownerOf 时应返回 Bob 的地址。
最后,我们用断言来检查 Bob 是否是新的拥有者。
测试一下
第一行 :调用 createRandomZombie,为 Alice 创建一个僵尸。给它 zombieNames[0] 作为名字,并确保 Alice 是该僵尸的所有者。
第二行 :声明一个常量 zombieId,并将其设置为僵尸的 ID。在第五章中,你学会了如何从日志和事件中获取信息。需要时,回顾一下相关内容。确保使用 toNumber() 将 zombieId 转换为有效的数字。
然后,我们需要调用 transferFrom,将 Alice 和 Bob 作为前两个参数传入。确保 Alice 调用此函数,并且在继续下一步之前等待它运行完毕。
声明一个名为 newOwner 的常量,并将其设置为调用 ownerOf,传入 zombieId。
最后,让我们检查 Bob 是否拥有该 ERC721 代币。我们可以通过运行 assert.equal(newOwner, bob) 来验证这一点。
注意 :assert.equal(newOwner, bob) 和 assert.equal(bob, newOwner) 基本上是一样的。但是我们的命令行解释器并不是很高级,因此除非你输入第一个选项,它不会认为你的答案正确。
我说过前一步是最后一步吗?好吧… 其实那是错的。最后我们要做的是通过移除 x 来“取消跳过”第一个场景。
呼!这已经是很多代码了。希望你能做对。如果没有,随时点击“显示答案”。
现在我们来运行 truffle test,看看新的测试是否通过: Contract: CryptoZombies ✓ should be able to create a new zombie (146 ms) ✓ should not allow two zombies (235 ms) with the single-step transfer scenario ✓ should transfer a zombie (382 ms) with the two -step transfer scenario - should approve and then transfer a zombie when the owner calls transferFrom - should approve and then transfer a zombie when the approved address calls transferFrom3 passing (1 s)2 pending
看!我们的代码成功通过了测试👏🏻。
在下一章,我们将继续进行两步场景,其中 approve 后跟 transferFrom。
答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const CryptoZombies = artifacts.require("CryptoZombies" ); const utils = require("./helpers/utils" ); 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.to Number() ; await contractInstance.transferFrom(alice , bob , zombieId , {from : alice }) ; const newOwner = await contractInstance.ownerOf(zombieId ) ; assert .equal(newOwner, bob); }) }) xcontext("with the two-step transfer scenario" , async () => { it("should approve and then transfer a zombie when the approved address calls transferFrom" , async () => { }) it("should approve and then transfer a zombie when the owner calls transferFrom" , async () => { }) }) })
第10章:ERC721 代币转移 - 两步场景 现在,使用 approve 后跟 transferFrom 的方式转移 ERC721 代币远不是一件轻松的事情,但我在这里帮助你。
简而言之,我们需要测试两种不同的场景:
Alice 批准 Bob 转移 ERC721 代币 ,然后由 Bob(被批准的地址)调用 transferFrom。
Alice 批准 Bob 转移 ERC721 代币 ,然后由 Alice 自己调用 transferFrom。
这两种场景的区别在于谁来执行实际的转移操作,是 Alice 还是 Bob。
看起来我们已经将其简化了,对吧?
让我们来看第一个场景: Bob 调用 transferFrom 这个场景的步骤如下:
Alice 创建一个新的 ERC721 代币,然后调用 approve。
接下来,Bob 执行 transferFrom,这应该使他成为该 ERC721 代币的所有者。
最后,我们需要调用 assert.equal,检查 newOwner 是否等于 Bob。
测试一下 我们测试的前两行代码和之前的测试类似。我们已经为你复制并粘贴了它们。
接下来,为了让 Bob 被批准转移 ERC721 代币,我们需要调用 approve()。该函数接收 Bob 和 zombieId 作为参数。并确保 Alice 调用这个方法(因为是她的 ERC721 代币将被转移)。
接下来的三行代码与之前的测试几乎相同。我们已经为你复制并粘贴了它们。让我们更新 transferFrom() 的调用,确保发送者是 Bob。
最后,让我们取消跳过这个场景,并跳过最后一个测试用例——那个我们还没有编写的测试。
运行 truffle test,看看我们的测试是否通过: Contract: CryptoZombies ✓ should be able to create a new zombie (218 ms) ✓ should not allow two zombies (175 ms) with the single-step transfer scenario ✓ should transfer a zombie (334 ms) with the two -step transfer scenario ✓ should approve and then transfer a zombie when the owner calls transferFrom (360 ms) - should approve and then transfer a zombie when the approved address calls transferFrom 4 passing (2 s) 1 pending
太棒了! 现在,我们进入下一个测试。
答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const CryptoZombies = artifacts.require("CryptoZombies" ); const utils = require("./helpers/utils" ); 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.to Number() ; 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 transferFrom" , async () => { const result = await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; const zombieId = result.logs[0 ] .args.zombieId.to Number() ; 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); }) xit("should approve and then transfer a zombie when the owner calls transferFrom" , async () => { }) }) })
第11章:ERC721 代币转移 - 两步场景(续) 我们快完成转移的测试了!现在我们来测试 Alice 调用 transferFrom 的场景。
有一个好消息——这个测试非常简单。你只需要复制并粘贴前一章节的代码,然后让 Alice(而不是 Bob)调用 transferFrom。
动手实践 复制并粘贴前一个测试的代码,并让 Alice 调用 transferFrom。 “取消跳过”这一部分,我们就完成了。
如果你运行 truffle test,输出将类似于以下内容:
Contract: CryptoZombies ✓ should be able to create a new zombie (201 ms) ✓ should not allow two zombies (486 ms) ✓ should return the correct owner (382 ms) with the single-step transfer scenario ✓ should transfer a zombie (337 ms) with the two -step transfer scenario ✓ should approve and then transfer a zombie when the approved address calls transferFrom (266 ms) 5 passing (3 s)
我想不出还有什么和转移相关的测试,所以我们暂时就到这里。
答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const CryptoZombies = artifacts.require("CryptoZombies" ); const utils = require("./helpers/utils" ); 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.to Number() ; 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 transferFrom" , async () => { const result = await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; const zombieId = result.logs[0 ] .args.zombieId.to Number() ; 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 transferFrom" , async () => { const result = await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; const zombieId = result.logs[0 ] .args.zombieId.to Number() ; 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); }) }) })
第12章:僵尸攻击 哇!前几章内容非常充实,我们学到了很多东西。
那么我们现在就完成了所有的场景吗?不,还没有。我们把最精彩的部分留到了最后。
我们已经构建了一个僵尸游戏,而最精彩的部分莫过于僵尸们互相战斗,对吧?
这个测试相当简单,包含以下步骤:
首先,我们为 Alice 和 Bob 各创建一个新的僵尸。
然后,Alice 使用 Bob 的僵尸 ID 作为参数运行攻击。
最后,为了使测试通过,我们将检查 result.receipt.status 是否为 true。
在这里,我已经快速写好了所有的逻辑,并将其包裹在 it() 函数中,然后运行了 truffle test。
输出看起来应该是这样的:
Contract: CryptoZombies ✓ should be able to create a new zombie (102 ms) ✓ should not allow two zombies (321 ms) ✓ should return the correct owner (333 ms) 1 ) zombies should be able to attack another zombie with the single-step transfer scenario ✓ should transfer a zombie (307 ms) with the two -step transfer scenario ✓ should approve and then transfer a zombie when the owner calls transferFrom (357 ms)
哎呀 ,我们的测试失败了☹️。
为什么会失败呢?
让我们来搞清楚。首先,我们来仔细看看 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 ], 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 ));
注意 :显然,时间旅行在主网或任何由矿工保护的测试链上都是不可用的。如果任何人都能随便改变现实世界的时间操作,那将会搞得一团糟!但对于测试智能合约来说,时间旅行是开发者工具箱中不可或缺的一部分。
测试一下 我们已经为你填充了那个失败的测试版本。
滚动到我们为你留下的注释。接下来,通过运行 await time.increase 修复测试用例,像上面所示那样。
我们一切准备好了。现在让我们运行 truffle test:
Contract: CryptoZombies ✓ should be able to create a new zombie (119 ms) ✓ should not allow two zombies (112 ms) ✓ should return the correct owner (109 ms) ✓ zombies should be able to attack another zombie (475 ms) with the single-step transfer scenario ✓ should transfer a zombie (235 ms) with the two -step transfer scenario ✓ should approve and then transfer a zombie when the owner calls transferFrom (181 ms) ✓ should approve and then transfer a zombie when the approved address calls transferFrom (152 ms)
就这样!这是本章的最后一步。
第13章:使用 Chai 进行更具表达力的断言 到目前为止,我们一直在使用内置的 assert 模块来编写断言。虽然 assert 模块并不差,但它有一个重大缺点——代码的可读性差。幸运的是,有几个更好的断言模块,其中 Chai 是最棒的之一。
Chai 断言库 Chai 非常强大,考虑到本课的范围,我们这里只是简单介绍一下。完成这节课后,随时可以查看他们的文档来进一步学习。
话虽如此,让我们来看看 Chai 中包含的三种断言风格:
1. expect : 允许你像下述那样链式调用自然语言的断言:
let lessonTitle = "Testing Smart Contracts with Truffle" ; expect(lessonTitle).to.be.a("string" );
2. should : 提供了类似 expect 接口的断言,但链式调用是从 should 属性开始的:
let lessonTitle = "Testing Smart Contracts with Truffle" ; lessonTitle.should.be.a("string" );
3. assert : 提供了与 node.js 中的断言类似的语法,并包含了几个附加的测试,同时也兼容浏览器:
let lessonTitle = "Testing Smart Contracts with Truffle" ; assert.typeOf(lessonTitle, "string" );
在本章中,我们将展示如何使用 expect 风格来改进你的断言。
注意 :我们假设你已经在你的电脑上安装了 chai 包。如果还没有,你可以通过以下命令轻松安装它:
要使用 expect 风格,首先我们需要将其导入到项目中,方法如下:
var expect = require ('chai' ).expect; expect().to.equal()
使用 expect 风格进行断言 现在我们已经将 expect 导入到项目中,检查两个字符串是否相等的代码如下所示:
let zombieName = 'My Awesome Zombie' ; expect(zombieName).to.equal('My Awesome Zombie' );
说够了,让我们好好利用 Chai 的强大功能吧!
测试一下
将 expect 导入到我们的项目中。
继续上面的 zombieName 示例,我们可以使用 expect 来测试交易是否成功,方法如下:
expect(result.receipt.status).to.equal(true );
我们可以检查 Alice 是否拥有一个僵尸,如下所示:
expect(zombieOwner).to.equal(alice);
将代码中所有的 assert.equal 替换为 expect。我们在代码中留下了很多注释,方便你找到需要替换的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const CryptoZombies = artifacts.require("CryptoZombies" ); const utils = require("./helpers/utils" ); const time = require("./helpers/time" ); var expect = require('chai').expect; 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 }) ; expect(result.receipt.status).to .equal(true ); expect(result.logs[0 ] .args.name).to .equal(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.to Number() ; await contractInstance.transferFrom(alice , bob , zombieId , {from : alice }) ; const newOwner = await contractInstance.ownerOf(zombieId ) ; expect(newOwner).to .equal(bob); }) }) context("with the two-step transfer scenario" , async () => { it("should approve and then transfer a zombie when the approved address calls transferFrom" , async () => { const result = await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; const zombieId = result.logs[0 ] .args.zombieId.to Number() ; await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice , bob , zombieId , {from : bob }) ; const newOwner = await contractInstance.ownerOf(zombieId ) ; expect(newOwner).to .equal(bob); }) it("should approve and then transfer a zombie when the owner calls transferFrom" , async () => { const result = await contractInstance.createRandomZombie(zombieNames [0], {from : alice }) ; const zombieId = result.logs[0 ] .args.zombieId.to Number() ; await contractInstance.approve(bob, zombieId, {from: alice}); await contractInstance.transferFrom(alice , bob , zombieId , {from : alice }) ; const newOwner = await contractInstance.ownerOf(zombieId ) ; expect(newOwner).to .equal(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.to Number() ; result = await contractInstance.createRandomZombie(zombieNames [1], {from : bob }) ; const secondZombieId = result.logs[0 ] .args.zombieId.to Number() ; await time.increase(time.duration.days(1 )); await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); expect(result.receipt.status).to .equal(true ); }) })
第14章:在 Loom 上进行测试 非常棒!你一定已经在不断练习了。
现在,本教程如果没有展示如何在 Loom Testnet 上进行测试,那就不完整了。
回想一下我们之前的课程,你应该记得在 Loom 上,用户可以享受比在 Ethereum 上更快速且无 gas 费用的交易。这使得 DAppChains 非常适合像游戏或面向用户的 DApp 这样的应用。
而且你知道吗?部署和在 Loom 上进行测试完全没有不同。我们已经总结了在 Loom 上进行测试所需的步骤,让我们快速看一下。
配置 Truffle 用于在 Loom 上测试 首先,首先要做的是告诉 Truffle 如何部署到 Loom Testnet。我们需要将以下代码片段放到 networks 对象中:
loom_testnet: { provider : function ( ) { const privateKey = 'YOUR_PRIVATE_KEY' ; const chainId = 'extdev-plasma-us1' ; const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket' ; const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws' ; return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); }, network_id : 'extdev' }
注意 :永远不要泄露你的私钥!我们这么做只是为了简单起见。更安全的做法是将你的私钥保存在一个文件中,并从文件中读取私钥的值。如果这样做,请确保不要将保存私钥的文件推送到 GitHub 上,这样任何人都能看到它。
accounts 数组为了让 Truffle 与 Loom 通信,我们将默认的 HDWalletProvider 替换成了我们自己的 Truffle Provider。因此,我们需要告诉我们的提供者填充 accounts 数组,以便我们能够测试我们的游戏。为了实现这一点,我们需要替换返回新 LoomTruffleProvider 的代码行:
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
用以下代码:
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10 );return loomTruffleProvider;
测试一下
替换返回新 LoomTruffleProvider 的代码行,使用上面提供的代码片段。
还有一件事需要注意:时间旅行(Time Traveling)仅在测试时适用于 Ganache,因此我们应该跳过这个测试。你已经知道如何通过在函数名之前加上 x 来跳过测试。但是,这次我们希望你学到新东西。简而言之,你可以通过简单地链式调用 skip() 函数来跳过一个测试,像这样:
it.skip("zombies should be able to attack another zombie" , async () => { })
我们已经为你跳过了该测试。接下来,我们运行了 truffle test --network loom_testnet。
如果你按照上面的命令输入,输出应该如下所示:
Contract: CryptoZombies ✓ should be able to create a new zombie (6153 ms) ✓ should not allow two zombies (12895 ms) ✓ should return the correct owner (6962 ms) - zombies should be able to attack another zombie with the single-step transfer scenario ✓ should transfer a zombie (13810 ms) with the two -step transfer scenario ✓ should approve and then transfer a zombie when the approved address calls transferFrom (22388 ms) 5 passing (2 m) 1 pending
总结 到这里,我们已经完成了在 Loom 上测试 CryptoZombies 智能合约的过程。
答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const HDWalletProvider = require ("truffle-hdwallet-provider" );const LoomTruffleProvider = require ('loom-truffle-provider' );const mnemonic = "YOUR MNEMONIC HERE" ;module .exports = { networks : { development : { host : "127.0.0.1" , port : 7545 , network_id : "*" , gas : 9500000 }, mainnet : { provider : function ( ) { return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/<YOUR_INFURA_API_KEY>" ) }, network_id : "1" }, rinkeby : { provider : function ( ) { return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/<YOUR_INFURA_API_KEY>" ) }, network_id : 4 }, loom_testnet : { provider : function ( ) { const privateKey = 'YOUR_PRIVATE_KEY' ; const chainId = 'extdev-plasma-us1' ; const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket' ; const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws' ; const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10 ); return loomTruffleProvider; }, network_id : '9545242630824' } }, compilers : { solc : { version : "0.4.25" } } };
你完成了游戏的测试。你真是太棒了!
尽管在这个案例中我们的游戏是为了演示而构建的,但很明显,测试一个 Solidity 智能合约并不是一件简单的事。但我们知道,现在你已经具备了测试智能合约的能力!
需要记住的重要事项是:
确保为游戏中的每个函数创建单独的测试。
保持代码清晰标注和组织。
利用时间旅行(time traveling)功能。
在开发游戏或面向用户的 DApp 时,考虑使用 Loom。我们强烈建议你从查看我们的文档开始。