欢迎!通过完成前面的课程,你已经证明了你真正了解你的东西。
因此,继续将游戏部署到主网。享受你的成功!
等一下……有一些事情你可能已经想到了。毕竟,一旦合约被部署到主网,它们将永远存在。如果其中有错误,它们也会继续存在。就像不死僵尸。
无论多么熟练,每个程序员都会遇到错误或bug。你不太可能犯一个大错误,比如给攻击僵尸100%的胜利机会,但这是可能的。
显然,给予进攻方100%的胜率将意味着你所写的内容不再是游戏,甚至没有任何乐趣。这样的漏洞可能会让你的游戏彻底失败,也不可能让你的僵尸死里逃生。
为了防止这种可怕的事情发生,你必须彻底测试游戏的每个方面。
本课结束时,你将能够:
- 用
Truffle
和Ganache
测试你的智能合约 - 使用Chai编写更具表现力的断言
- Loom测试😉
我们开始吧
Chapter 1: 准备就绪
在本课中,我们将介绍测试以太坊智能合约背后的理论,重点是Truffle,Ganache
和Chai
。你将需要一个中等水平的Solidity和JavaScript知识来帮助你从这些课程中获得最大的收获。
如果你之前没有接触过Solidity,或者你想复习一些概念,继续并开始学习我们的第一课。
如果您不熟悉JavaScript,请考虑在开始本课之前先阅读其他教程。
看看我们的计划
如果你遵循我们之前的课程,你应该已经建立了一个僵尸主题的游戏,基本上准备好了,你的文件结构应该是这样的:
├── build
├── contracts
├── Migrations.json
├── CryptoZombies.json
├── erc721.json
├── ownable.json
├── safemath.json
├── zombieattack.json
├── zombiefactory.json
├── zombiefeeding.json
├── zombiehelper.json
├── zombieownership.json
├── contracts
├── Migrations.sol
├── CryptoZombies.sol
├── erc721.sol
├── ownable.sol
├── safemath.sol
├── zombieattack.sol
├── zombiefactory.sol
├── zombiefeeding.sol
├── zombiehelper.sol
├── zombieownership.sol
├── migrations
└── test
. package-lock.json
. truffle-config.js
. truffle.js
看到test
文件夹了吗?这就是我们要进行测试的地方。
Truffle为用JavaScript和Solidity编写的测试提供支持,但是,对于本课的范围,我们将保持简单并坚持使用JavaScript。
实战演习
最佳实践是为每个合约创建一个单独的测试文件,并为其指定智能合约的名称。这使得在长期运行中更容易管理测试,特别是当项目增长和更改时。
- 在右侧终端运行`
Chapter 2: 开始设置(续)
让我们继续前进。在本章中,我们将继续进行设置,以便编写和运行测试。
Build Artifacts
每次编译智能合约时,Solidity编译器都会生成一个JSON文件(称为构建工件),其中包含该合约的二进制表示,并将其保存在build/contracts文件夹中。
接下来,在运行迁移时,Truffle使用与该网络相关的信息更新该文件。
每次开始编写新的测试套件时,您需要做的第一件事就是加载您想要与之交互的合约的构建工件。通过这种方式,Truffle将知道如何以合约能够理解的方式格式化函数调用。
让我们看一个简单的例子。
假设有一份名为MyAwesomeContract
的合同。我们可以做如下的事情来加载构建工件:
const MyAwesomeContract = artifacts.require(“MyAwesomeContract”);
该函数返回一个称为contract abstraction
的东西。简而言之,contract abstraction
隐藏了与以太坊交互的复杂性,并为我们的Solidity智能合约提供了方便的JavaScript接口。我们将在接下来的章节中使用它。
The contract() function
在幕后,为了使测试更简单,Truffle在Mocha周围添加了一个薄包装。由于我们的课程侧重于以太坊开发,因此我们不会花太多时间解释Mocha的位和字节。如果你想了解更多关于摩卡的知识,一旦你完成了这节课,请访问他们的网站。现在,你只需要了解我们在这里介绍的内容——如何:
- 通过调用名为contract()的函数对测试进行分组。它扩展了Mocha的describe(),提供了一个用于测试和做一些清理的帐户列表。
- Contract()接受两个参数。第一个,一个字符串,必须指示我们要测试什么。第二个参数,一个回调,是我们实际编写测试的地方。
- 执行它们:我们要做的是调用一个名为it()的函数,它也有两个参数:一个描述测试实际做什么的字符串和一个回调。
把它们放在一起,这里有一个简单的测试:
contract("MyAwesomeContract", (accounts) => {
it("should be able to receive Ethers", () => {
})
})
注意:一个经过深思熟虑的测试解释了代码实际做了什么。确保测试套件和测试用例的描述可以作为一个连贯的陈述一起阅读。这就像你在写文档。
您想要编写的每个测试都遵循这个简单的模式。很简单,不是吗?😁
实战演习
我们已经创建了一个空的CryptoZombies.js
文件,让我们来填充它。
- 第一行代码应该声明一个名为
CryptoZombies
的const
,给他赋值为artifacts.require
的结果,将我们要测试的合约的名称作为参数。 - 接下来,从上面复制/粘贴测试。
- 修改调用
contract()
的方式,使第一个参数是我们的智能合约的名称。
注意:不要担心
accounts
。我们将在下一章解释它。
- 传递给it()函数的第一个参数(在我们的示例中,即“应该能够接收以太币”)应该是我们测试的名称。由于我们将从创建一个新的僵尸开始,请确保第一个参数设置为“应该能够创建一个新的僵尸”。
都准备好了。让我们进入下一章。
代码更新
test/CryptoZombies.js
const CryptoZombies = artifacts.require("CryptoZombies");
contract("CryptoZombies", (accounts) => {
it("should be able to create a new zombie", () => {
})
})
Chapter 3: 第一个测试-创造一个新的僵尸
在部署到以太坊之前,最好在本地测试您的智能合约。
您可以通过使用Ganache工具来实现这一点,该工具可以设置本地以太坊网络。
每次Ganache启动时,它都会创建10个测试帐户,并给他们100个以太币,以使测试更容易。由于Ganache和Truffle是紧密结合在一起的,我们可以通过我们在前一章提到的accounts数组访问这些帐户。
但是使用帐户[0]和帐户[1]不会使我们的测试读起来很好,对吗?
let [alice, bob] = accounts;
注意:请原谅语法错误。在JavaScript中,惯例是使用小写的变量名。
为什么是爱丽丝和鲍勃?爱丽丝和鲍勃或“a和B”在密码学、物理学、编程等领域都是常用的名字,这是一个很大的传统。这是一段简短但有趣的历史,非常值得你在完成本课后阅读。
现在让我们继续我们的第一个测试。
Creating a New Zombie
假设Alice想玩我们的游戏。如果是这样,她想做的第一件事就是创造自己的僵尸🧟。要做到这一点,前端(在我们的例子中是Truffle)必须调用createRandomZombie函数。
注:作为回顾,以下是我们合同中的Solidity代码:
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
我们从测试这个函数开始
实战演习
- contract()函数的第一行应该声明两个变量alice和bob,并如上所示对它们进行初始化。
- 接下来,我们想要正确地调用it()函数。第二个参数(一个回调函数)将与区块链“对话”,这意味着该函数是异步的。只要在async关键字前加上即可。这样,每次使用await关键字调用该函数时,我们的测试都会等待它返回。
解释promises如何起作用不在本课的范围之内。一旦你完成了这一课,请随意查看官方文档以进一步了解你的知识。
代码更新
const CryptoZombies = artifacts.require("CryptoZombies");
contract("CryptoZombies", (accounts) => {
//1. initialize `alice` and `bob`
let [alice, bob] = accounts;
it("should be able to create a new zombie", async () => { //2 & 3. Replace the first parameter and make the callback async
})
})
Chapter 4:第一个测试-创造一个新的僵尸(续)
伟大的工作!现在我们有了第一个测试的shell,让我向您介绍测试是如何工作的。
通常,每个测试都有以下几个阶段:
set up
:其中我们定义初始状态并初始化输入。act
:我们实际测试代码的地方。一定要确保你只测试一件事。Assert
:检查结果的地方。
让我们更详细地看看我们的测试应该做些什么
1. set up
在第2章中,您学习了如何创建contract abstraction。然而,contract abstraction,顾名思义,只是一种抽象。为了实际与我们的智能合约进行交互,我们必须创建一个JavaScript对象,作为合约的实例。继续MyAwesomeContract
的例子,我们可以使用合约抽象来初始化我们的实例,像这样:
const contractInstance = await MyAwesomeContract.new();
很好,接下来是什么?
调用createRandomZombie
需要我们将僵尸的名字作为参数传递给它。所以,下一步就是给爱丽丝的僵尸取个名字。比如“爱丽丝的超级僵尸”。
然而,如果我们对每个测试都这样做,我们的代码看起来就不美观了。一个更好的方法是初始化一个全局数组,如下所示:
const zombieNames = ["Zombie #1", "Zombie #2"];
然后,像这样调用合约的方法:
contractInstance.createRandomZombie(zombieNames[0]);
注意:使用一个数组来存储僵尸的名字是很方便的,例如,如果您想编写一个测试,创建1000个僵尸,而不是一个或两个😉。
实战演习
我们已经为您初始化了zombinames
数组。
- 让我们创建一个合约的实例。声明一个名为
contractInstance
的新const
,并将其设置为与CryptoZombies.new()
函数的结果相等。 CryptoZombies.new ()
与区块链“对话”。这意味着它是一个异步函数。让我们在函数调用之前添加await
关键字。
代码更新
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
let [alice, bob] = accounts;
it("should be able to create a new zombie", async () => {
// start here
const contractInstance = await CryptoZombies.new();
})
})
Chapter 5:第一个测试-创造一个新的僵尸(续)
现在我们已经把鸭子——嗯哼,僵尸——排好了,让我们进入下一个阶段……🧟🦆🧟🦆🧟🦆🧟🦆🧟🦆🧟🦆
2. Act
我们已经到达了将要调用为Alice创建一个新僵尸的函数的部分——createRandomZombie。
但是有一个小问题——我们如何使方法“知道”是谁调用它?另一种说法是——我们如何确保爱丽丝(而不是鲍勃)将成为这个新僵尸的主人?🧐
嗯…用contract abstraction来解决这个问题。Truffle的一个特性是它封装了原始的Solidity实现,并允许我们通过将该地址作为参数传递来指定进行函数调用的地址。
下面调用createRandomZombie并确保msg。sender被设置为Alice的地址:
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
现在我有一个简单的问题要问你:你知道result中存储的是什么吗?
好吧,让我解释一下。
Logs and Events
一旦我们指定了我们想要使用工件进行测试的合约。要求时,Truffle会自动提供由我们的智能合约生成的日志。这意味着我们现在可以使用如下代码检索Alice新创建的僵尸的名称:result.logs[0].args.name。以类似的方式,我们可以获得id和_dna。
除了这些信息,结果还会给我们一些关于交易的其他有用的细节:
result.tx
: 交易哈希result.receipt
: 包含交易收据的对象。如果result. receive .status
等于true,则表示事务成功。否则,意味着事务失败。
请注意,日志也可以用作存储数据的更便宜的选项。缺点是它们不能从智能合约本身内部访问。
3. Assert
在本章中,我们将使用内置断言模块,该模块附带一组断言函数,如equal()
和deepEqual()
。简单地说,这些函数检查条件,如果结果不符合预期就抛出错误。因为我们将比较简单的值,所以我们将运行assert.equal()
。
实战演习
让我们完成第一个测试。
-
声明一个名为result的const,并将其设置为contractInstance的结果。createRandomZombie,使用僵尸的名字和所有者作为参数。
-
得到结果后,调用assert。等于两个参数- result. receive .status和true。
如果上述条件成立,我们可以假设我们的测试已经通过。为了安全起见,我们在这里再加一个检查。 -
在下一行中,检查result.logs[0].args.name是否等于zombieNames[0]。使用断言。等于,和上面一样。
现在,是时候进行Truffle测试了看看我们的第一次测试是否通过。它的工作方式是,Truffle将检查“test”目录并执行它在那里找到的文件。
事实上,我们已经为你做过了。输出应该看起来像这样:
Contract: CryptoZombies
✓ should be able to create a new zombie (323ms)
1 passing (768ms)
这就是你的第一个测试——做得好!还有更多的内容,所以让我们继续下一课……
代码更新
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
let [alice, bob] = accounts;
it("should be able to create a new zombie", async () => {
const contractInstance = await CryptoZombies.new();
// start here
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
assert.equal(result.receipt.status, true);
assert.equal(result.logs[0].args.name,zombieNames[0])
})
})
Chapter 6: 保持游戏的乐趣
到目前为止,工作非常棒!现在我们确定我们的用户可以创建新的僵尸👌🏻。
然而,如果他们可以不断调用这个函数在他们的军队中创造无限的僵尸,游戏就不会很有趣了。因此,在第2课的第4章中,我们在createZombieFunction()中添加了一条require语句,以确保每个用户不能拥有多个僵尸:
require(ownerZombieCount[msg.sender] == 0)
让我们测试一下这个特性,看看它是否有效。
Hooks
在短短几分钟内🤞,我们将有多个测试,其工作方式是每个测试都应该从一张干净的纸开始。因此,对于每一个测试,我们都必须创建一个智能合约的新实例,如下所示:
const contractInstance = await CryptoZombies.new();
如果您可以只编写一次,并让Truffle在每次测试时自动运行它,这不是很好吗?
嗯…Mocha(和Truffle)的特性之一是能够在测试之前或之后运行一些称为hooks的代码片段。要在执行测试之前运行某些内容,应该将代码放入名为beforeEach()的函数中。
因此,不用多次编写contract.new(),只需像这样执行一次即可:
beforeEach(async () => {
// let's put here the code that creates a new contract instance
});
然后,Truffle会照顾好一切。真贴心,不是吗?
实战演习
- 在初始化alice和bob的代码行下面,让我们声明一个名为
contractInstance
的变量。不要把它分配给任何东西。
注意:我们希望将contractInstance的作用域限制在定义它的块中。使用let代替var。
- 接下来,复制/粘贴上面用于定义
beforeEach()
函数的代码片段。 - 让我们来填充新函数的函数体。继续移动在
beforeEach()
函数中创建新契约实例的代码行。既然我们已经在别处定义了contractInstance
,就可以删除const限定符了。 - 我们需要一个新的空
it
函数来进行测试。将测试的名称(这是我们传递给it
函数的第一个参数)设置为“should not allow two zombies”。
代码更新
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
let [alice, bob] = accounts;
// start here
let contractInstance;
beforeEach(async () => {
// let's put here the code that creates a new contract instance
contractInstance = await CryptoZombies.new();
});
it("should be able to create a new zombie", async () => {
//const contractInstance = await CryptoZombies.new();
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
assert.equal(result.receipt.status, true);
assert.equal(result.logs[0].args.name,zombieNames[0]);
})
//define the new it() function
it("should not allow two zombies", async () => {
})
})
🧟♂️Here be… zombies of every kind!!!🧟♂️
如果你真的,真的想要精通,那就继续往下读吧。否则……只要点击下一步,你就可以进入下一章。
你还在吗?😁
太棒了!毕竟,你为什么要否认自己有很多很棒的东西呢?
现在,让我们回到contract.new
如何工作。基本上,每次我们调用这个函数时,Truffle都会使它部署一个新的合约。
一方面,这是有帮助的,因为它让我们以一张白纸开始每个测试。
另一方面,如果每个人都创建无数的合同,区块链将变得臃肿。我们希望您留在这里,但不是您的旧测试合同!
我们想要防止这种情况发生,对吧?
令人高兴的是,解决方案非常简单……一旦不再需要,我们的合同就会selfdestruct
。
其工作方式如下:
- 首先,我们想在CryptoZombies智能合约中添加一个新功能,如下所示:
function kill() public onlyOwner {
selfdestruct(owner());
}
注意:如果你想了解更多关于self - destruct()的知识,你可以在这里阅读Solidity文档。要记住的最重要的事情是,自毁是将特定地址的代码从区块链中删除的唯一方法。这使得它成为一个非常重要的特性!这里是引用
- 接下来,与上面解释的
beforeEach()
函数有点类似,我们将创建一个名为afterEach()
的函数:
afterEach(async () => {
await contractInstance.kill();
});
最后,Truffle将确保在执行测试后调用该函数。
瞧,智能合约自己消失了!
在本课中我们有很多内容要介绍,实现这个特性可能需要至少2个额外的章节。所以,我们相信你能添加它💪🏻
Chapter 7: 保持游戏乐趣(续)
在本章中,我们将填充第二个测试的主体。这是它应该做的:
- 首先,Alice应该调用createRandomZombie,并给它zombieNames[0]作为她的第一个僵尸的名字。
- 接下来,爱丽丝应该尝试创造她的第二个僵尸。唯一不同的是,这一次,僵尸名称应该设置为zombieNames[1]。
- 此时,我们希望合约抛出一个错误。
因为只有当智能合约出错时,我们的测试才会通过,所以我们的逻辑看起来有点不同。我们必须将第二个createRandomZombie函数调用封装在try/catch 块中:
try {
//try to create the second zombie
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());
实战演习
在前一章中,我们为第二个测试创建了一个空shell。我们把它填上。
- 首先,让爱丽丝创造她的第一个僵尸。将其命名为zombieNames[0],不要忘记正确设置所有者。
- 在Alice创建了她的第一个僵尸之后,使用
createRandomZombie
作为参数运行shouldThrow
。如果您不记得这样做的语法,请查看上面的示例。但首先,不要偷看。
太棒了,你刚刚完成了第二个测试!
现在,我们已经为你做了松露测试。下面是输出:
Contract: CryptoZombies
✓ should be able to create a new zombie (129ms)
✓ should not allow two zombies (148ms)
2 passing (1s)
测试通过了。万岁!
代码更新
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 () => {
// start here
await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
})
})
Chapter 8: Zombie Transfers
问题-假设Alice想把她的僵尸送给Bob。我们要测试一下吗?
肯定的!
如果您一直遵循前面的课程,那么您应该知道,除其他事项外,我们的僵尸继承自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首先使用Bob的地址和僵尸id呼叫approve。然后合约存储Bob被批准带走僵尸的信息。接下来,当Alice或Bob调用transferFrom时,合约检查该msg.sender是否等于Alice或Bob的地址。如果是,它将僵尸转移给Bob。
我们将这两种转移僵尸的方式称为“场景”。为了测试每个场景,我们需要创建两组不同的测试,并为它们提供有意义的描述。
为什么把它们分组?我们只有几个测试…
是的,现在我们的逻辑很简单,但情况可能并不总是如此。然而,第二种情况(批准之后是transferFrom)至少需要两个测试:
- 首先,我们必须检查爱丽丝自己是否有能力转移僵尸。
- 其次,我们还必须检查是否允许Bob运行transferFrom。
此外,在将来,您可能希望添加需要不同测试的其他功能。我们认为最好从一开始就建立一个可扩展的结构😉。它使外人更容易理解您的代码,或者对于您自己来说,如果您花了一段时间专注于其他事情的话。
注意:如果您最终处于与其他编码人员一起工作的位置,您会发现他们更有可能遵循您在初始代码中制定的约定。如果你想在大项目上取得成功,那么有效的协作能力是你需要的关键技能之一。尽早养成能帮助你做到这一点的好习惯,会让你的程序员生活更轻松、更成功。
The context function
为了分组测试,Truffle提供了一个名为context的功能。让我快速向你展示如何使用它,以便更好地构建我们的代码:
context("with the single-step transfer scenario", async () => {
it("should transfer a zombie", async () => {
// TODO: Test the single-step transfer scenario.
})
})
context("with the two-step transfer scenario", async () => {
it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
如果我们将其添加到CryptoZombies.js文件中,然后运行truffle test,输出将类似于这样:
Contract: CryptoZombies
✓ should be able to create a new zombie (100ms)
✓ should not allow two zombies (251ms)
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 (2s)
好吗?
嗯…
再看一遍——上面的输出有一个问题。看起来好像所有的测试都通过了,这显然是错误的,因为我们甚至还没有写它们!!
幸运的是,有一个简单的解决方案—如果我们只是在context()函数前面放置一个x
,如下所示:xcontext()
,那么Truffle将跳过这些测试。
注意:x也可以放在it()函数的前面。在编写这些函数的测试时,不要忘记删除所有的x !
现在,我们来做Truffle测试。输出应该看起来像这样:
Contract: CryptoZombies
✓ should be able to create a new zombie (199ms)
✓ should not allow two zombies (175ms)
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 (827ms)
3 pending
其中“-”表示用“x”标记跳过的测试。
很整洁,是吧?现在您可以运行您的测试,并标记出您知道在不久的将来需要编写测试的空函数。
实战演习
- 继续复制/粘贴上面的代码。
- 现在,让我们跳过新的上下文函数。
我们的测试只是空壳,为了实现它们需要编写大量的逻辑。在接下来的章节中,我们将以更小的部分来做。
代码更新
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}));
})
// start here
xcontext("with the single-step transfer scenario", async () => {
it("should transfer a zombie", async () => {
// TODO: Test the single-step transfer scenario.
})
})
xcontext("with the two-step transfer scenario", async () => {
it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
})
Chapter 9: ERC721 Token 交易-单步场景
到目前为止,我们只是在热身……
但现在是时候真正展示你所知道的了!
在接下来的章节中,我们将把我们所学到的东西放在一起,并测试一些非常酷的东西。
首先,我们将测试Alice将她的ERC721 Token传输给Bob的场景,只需一步。
下面是我们的测试应该做的事情:
- 为Alice创建一个新的僵尸(请记住,僵尸只不过是一个ERC721 Token)
- 让Alice将她的ERC721 Token转移给Bob。
- 此时,Bob应该拥有ERC721 Token。如果是,ownerOf将返回一个等于Bob地址的值。
- 让我们通过检查Bob是否是assert中的newOwner来结束它。
实战演习
- 函数的第一行应该调用createRandomZombie。将其命名为zombieNames[0],并确保Alice是所有者。
- 第二行应该声明一个名为zombieId的const,并将其设置为僵尸的id。在第五章中,我们学习了如何从智能合约的日志和事件中检索信息。如果需要的话,刷新你的记忆。确保还使用tonnumber()将zombieId转换为一个有效的数字。
- 然后,我们必须以alice和bob作为第一个参数调用transferFrom。确保Alice调用了这个函数,我们等待它完成运行,然后移动到下一步。
- 声明一个名为newOwner的const。将其设置为与zombieId调用的ownerOf相等。
- 最后,让我们检查Bob是否拥有这个ERC721令牌。在代码中,这意味着我们应该运行assert。用newOwner和bob作为参数;
注意:assert.equal(newOwner, bob) 和assert.equal(bob, newOwner)基本上是一样的。但是我们的命令行解释器不是很高级,所以它不会认为你的答案是正确的,除非你输入第一个选项。
- 我说过前一步是最后一步吗?嗯…这是个谎言。我们要做的最后一件事是通过删除x来“取消跳过”第一个场景。
唷!这是大量的代码。希望你能做对。如果没有,请点击“显示答案”。
现在让我们运行Truffle测试,看看我们的新测试是否通过:
Contract: CryptoZombies
✓ should be able to create a new zombie (146ms)
✓ should not allow two zombies (235ms)
with the single-step transfer scenario
✓ should transfer a zombie (382ms)
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
3 passing (1s)
2 pending
它就在那儿!我们的代码非常出色地通过了测试👏🏻。
在下一章中,我们将继续讨论两个步骤的场景,在这个场景中,approve
之后transferFrom
。
代码更新
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 () => {
// start here.
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);
})
})
xcontext("with the two-step transfer scenario", async () => {
it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
})
Chapter 10: ERC721 Token 交易-两步场景
现在,从转移ERC721 Token的方式来看,这远不是在公园散步,但我在这里提供帮助。
简而言之,我们需要测试两种不同的场景
- Alice批准Bob使用ERC721 Token。然后,Bob(批准的地址)调用
transferFrom
。 - Alice批准Bob使用ERC721 Token。接下来,Alice传输ERC721 Token。
这两种情况的区别在于谁调用了实际的转移,是Alice还是Bob。
我们让它看起来很简单,对吧?
让我们看一下第一种场景。
Bob calls transferFrom
该场景的步骤如下:
Alice创建一个新的ERC721 Token,然后调用approve。
接下来,Bob运行transferFrom,这将使他成为EC721 Token的所有者。
最后,我们必须调用assert.equal
。用newOwner
和bob
作为参数。
实战演习
- 我们测试的前两行代码与前面的测试类似。我们已经复制粘贴好了。
- 接下来,为了批准Bob使用ERC721 Token,调用approve()。该函数以bob和zombieId作为参数。另外,确保Alice调用了该方法(因为要传输的是她的ERC721 Token)。
- 最后三行代码几乎与前面的测试相似。同样,我们已经为你们复制粘贴了它们。让我们更新transferFrom()函数调用,使发送方为Bob。
- 最后,让我们“取消跳过”这个场景并“跳过”那个场景
现在运行truffle test
,看看我们的测试是否通过:
Contract: CryptoZombies
✓ should be able to create a new zombie (218ms)
✓ should not allow two zombies (175ms)
with the single-step transfer scenario
✓ should transfer a zombie (334ms)
with the two-step transfer scenario
✓ should approve and then transfer a zombie when the owner calls transferFrom (360ms)
- should approve and then transfer a zombie when the approved address calls transferFrom
4 passing (2s)
1 pending
太棒了!现在,让我们进行下一个测试。
代码更新
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.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 transferFrom", async () => {
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
const zombieId = result.logs[0].args.zombieId.toNumber();
// start here
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 () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
})
Chapter 11: ERC721 Token 交易-两步场景(续)
我们快完成传输测试了!现在让我们测试Alice调用transferFrom的场景。
我们有一些好消息要告诉你——这个测试很简单。你所要做的就是复制和粘贴前一章的代码,让Alice(不是Bob)调用transferFrom:
实战演习
复制并粘贴前面测试中的代码,并让Alice调用transferFrom。
"取消跳过"就搞定了。
如果运行truffle test
,输出将类似如下:
Contract: CryptoZombies
✓ should be able to create a new zombie (201ms)
✓ should not allow two zombies (486ms)
✓ should return the correct owner (382ms)
with the single-step transfer scenario
✓ should transfer a zombie (337ms)
with the two-step transfer scenario
✓ should approve and then transfer a zombie when the approved address calls transferFrom (266ms)
5 passing (3s)
我想不出任何其他与传输相关的测试,所以我们现在就到此为止。
代码更新
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.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 transferFrom", 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 transferFrom", 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);
})
})
})
Chapter 12: 僵尸攻击
哇!前面的章节包含了大量的信息,但是我们涵盖了很多内容。
所有的情况都讲完了吗?不,我们还没到那一步;我们把最好的东西留到最后。
我们创造了一款僵尸游戏,而最精彩的部分便是僵尸之间的战斗,对吧?
这个测试非常简单,包括以下步骤:
- 首先,我们将创建两个新的僵尸——一个给爱丽丝,另一个给鲍勃。
- 其次,Alice将使用Bob的zombiid作为参数对她的僵尸运行
attack
- 最后,为了让测试通过,我们将检查
result. receive .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天才能再次攻击。
如果没有这一点,僵尸每天可能会攻击并繁殖无数次,这将使游戏变得过于简单。
现在,我们该怎么办呢?等一天?
Time Travelling
幸运的是,我们不用等那么久。事实上,根本不需要等待。这是因为Ganache通过两个辅助函数提供了一种时间向前移动的方法:
- evm_increetime:增加下一个block的时间。
- Evm_mine:挖掘一个新的区块。
你甚至不需要Tardis或DeLorean来进行这种Time traveling。
让我来解释一下这些函数是如何工作的: - 每次挖出一个新的区块,矿工都会给它添加一个时间戳。假设创造僵尸的交易是在区块5挖出来的。
- 接下来,我们调用evm_increetime,但是,由于区块链是不可变的,因此无法修改现有块。因此,当合同检查时间时,它不会增加。
- 如果我们运行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. increetime (86400);
是啊,还是不够好。毕竟,我们真的希望你能马上知道一天有多少秒吗?
当然不是。这就是为什么我们添加了另一个名为days的辅助函数,它将我们想要增加时间的天数作为参数。你可以这样调用这个函数:await time.increase(time.duration.days(1))
注意:显然,Time traveling在主网或任何由矿工保护的可用测试链上都是不可用的。如果有人可以选择改变现实世界中时间的运行方式,那将会把事情搞得一团糟!对于测试智能合约,Time traveling可能是编码员曲目的重要组成部分。
实战演习
我们已经继续并填写了失败的测试版本。
向下滚动到我们为你留下的评论。接下来,如上所示增加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 transferFrom (181ms)
✓ should approve and then transfer a zombie when the approved address calls transferFrom (152ms)
这就对了!这是本章的最后一步。
代码更新
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 transferFrom", 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 transferFrom", 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 time.increase(time.duration.days(1));
await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
assert.equal(result.receipt.status, true);
})
})
Chapter 13: 更具表现力的断言 Chai
到目前为止,我们一直在使用内置的assert模块来编写断言。虽然不错,但assert模块有一个主要缺点——代码不太好读。幸运的是,市面上有几个更好的断言模块,Chai是其中最好的一个。
Chai Assertion Library
Chai非常强大,对于这节课的范围,我们只会触及表面。一旦你完成了这一课,请随意查看他们的指南,以进一步了解你的知识。
也就是说,让我们来看看捆绑在Chai中的三种断言风格:
- Expect:让你像下面这样链接自然语言断言:
let lessonTitle = "Testing Smart Contracts with Truffle";
expect(lessonTitle).to.be.a("string");
- Should:允许类似于expect接口的断言,但该链以Should属性开始:
let lessonTitle = "Testing Smart Contracts with Truffle";
lessonTitle.should.be.a("string");
- Assert:提供了一个类似于node.js打包的符号,包括几个额外的测试,并且它是浏览器兼容的:
let lessonTitle = "Testing Smart Contracts with Truffle";
assert.typeOf(lessonTitle, "string");
在本章中,我们将向您展示如何使用expect
改进断言。
注意:我们假设你的电脑上已经安装了chai
包。如果不是这样,你可以像这样安装它:npm -g install chai
为了使用expect
样式,我们应该做的第一件事是将它导入到我们的项目中,如下所示:
var expect = require('chai').expect;
expect().to.equal()
现在我们已经将expect导入到项目中,检查两个字符串是否相等将如下所示:
let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');
实战演习
将expect
导入到项目中。
使用zombieName
继续上面的例子,我们可以使用expect
来测试一个成功的事务,如下所示:
expect(result.receipt.status).to.equal(true);
我们可以检查Alice是否拥有这样的僵尸:
expect(zombieOwner).to.equal(alice);
用expect替换所有出现的assert。我们在代码中留下了大量注释,以便于查找。
代码更新
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const time = require("./helpers/time");
//TODO: import expect into our project
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});
//TODO: replace with expect
expect(result.receipt.status).to.equal(true);
expect(result.logs[0].args.name).to.equal(zombieNames[0]);
// 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);
//TODO: replace with expect
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.toNumber();
await contractInstance.approve(bob, zombieId, {from: alice});
await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
const newOwner = await contractInstance.ownerOf(zombieId);
//TODO: replace with expect
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.toNumber();
await contractInstance.approve(bob, zombieId, {from: alice});
await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
const newOwner = await contractInstance.ownerOf(zombieId);
//TODO: replace with expect
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.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});
//TODO: replace with expect
expect(result.receipt.status).to.equal(true);
//assert.equal(result.receipt.status, true);
})
})
Chapter 14: Testing Against Loom
令人印象深刻的!你一定一直在练习。
现在,如果不向您展示如何对Loom Testnet进行测试,本教程将是不完整的。
回想一下我们之前的课程,在Loom上,用户可以比在以太坊上进行更快、更无gas的交易。这使得DAppChains更适合游戏或面向用户的DApp。
你知道吗?在Loom上部署和测试并没有什么不同。我们已经总结了需要做些什么,这样你就可以对Loom进行测试。让我们快速浏览一下。
Configure Truffle for Testing on Loom
重要的事情先做。让我们通过在网络对象中放置以下代码片段来告诉Truffle如何部署到Loom Testnet。
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,任何人都可以看到它。
The accounts array
为了让Truffle与Loom“对话”,我们用自己的Truffle Provider替换了默认的HDWalletProvider。因此,我们必须告诉我们的提供商填写帐户数组,这样我们才能测试我们的游戏。为了做到这一点,我们需要替换返回一个新的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 () => {
//We're skipping the body of the function for brevity
})
我们已经为你跳过了测试。然后,我们运行truffle test --network loom_testnet
loom_testnet。
如果你从上面输入命令,输出应该是这样的:
Contract: CryptoZombies
✓ should be able to create a new zombie (6153ms)
✓ should not allow two zombies (12895ms)
✓ should return the correct owner (6962ms)
- zombies should be able to attack another zombie
with the single-step transfer scenario
✓ should transfer a zombie (13810ms)
with the two-step transfer scenario
✓ should approve and then transfer a zombie when the approved address calls transferFrom (22388ms)
5 passing (2m)
1 pending
现在就到这里,伙计们!我们已经完成了对CryptoZombies智能合约的测试。
代码更新
const HDWalletProvider = require("truffle-hdwallet-provider");
const LoomTruffleProvider = require('loom-truffle-provider');
const mnemonic = "YOUR MNEMONIC HERE";
module.exports = {
// Object with configuration for each network
networks: {
//development
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
gas: 9500000
},
// Configuration for Ethereum Mainnet
mainnet: {
provider: function() {
return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/<YOUR_INFURA_API_KEY>")
},
network_id: "1" // Match any network id
},
// Configuration for Rinkeby Metwork
rinkeby: {
provider: function() {
// Setting the provider with the Infura Rinkeby address and Token
return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/<YOUR_INFURA_API_KEY>")
},
network_id: 4
},
// Configuration for Loom Testnet
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';
// TODO: Replace the line below
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;
},
network_id: '9545242630824'
}
},
compilers: {
solc: {
version: "0.4.25"
}
}
};
Ending
你完成了游戏测试。你真是个了不起的人!
尽管在这种情况下,我们的游戏是为了演示目的而构建的,但很明显,测试Solidity智能合约绝非易事。但我们知道,现在你已经准备好测试你的智能合约了!
重要的是要记住:
确保你为游戏中的每个功能创建单独的测试。
把每件东西都清晰地贴上标签并组织起来
利用时间旅行
在开发游戏或面向用户的DApp时考虑使用Loom。我们强烈建议您从查看我们的文档开始。