我有一壶酒,足以慰平生。

0%

使用truffle测试智能合约


引言:这部分的教程网址没做中文页面,我自己英语不太好😫,就用谷歌翻译,翻译了下,大家凑合看吧。英语好的同学可以点击这里去网站学习。

Getting Set Up

在本课程中,我们将介绍测试以太坊智能合约背后的理论,重点是TruffleMochaChai。您将需要对SolidityJavaScript有中等程度的了解,以帮助您充分利用这些课程。

如果您没有接触Solidity的经验,或者想修改某些概念,请继续学习我们的第一课

如果您不熟悉JavaScript,请在开始本课程之前考虑阅读其他地方的教程。

让我们深入研究我们的项目

如果您按照我们之前的课程学习,那么您应该已经构建了一个以僵尸为主题的游戏,并且已经准备就绪,并且文件结构应如下所示:

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
├── 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支持使用JavaScriptSolidity编写的测试,但是,在本课程的范围内,我们将使事情变得简单并坚持使用JavaScript

进行测试

最佳实践是为每个合约创建一个单独的测试文件,并为其指定智能合约的名称。从长远来看,这使测试的管理变得更加简单,尤其是随着项目的增长和变更。

  1. 在右侧的终端中,运行touch test/CryptoZombies.js

如下图所示:

image-20200630145051119

Getting Set Up (cont’d)

让我们前进。在本章中,我们将继续进行设置,以便我们可以编写和运行测试。

建立Artifacts

每次编译智能合约时,Solidity编译器都会生成一个JSON文件(称为构建工件),其中包含该合约的二进制表示形式并将其保存在build/contracts文件夹中。

接下来,当您运行迁移时,Truffle会使用与该网络相关的信息来更新此文件。

每次开始编写新的测试套件时,您需要做的第一件事是加载要与之交互的合同的构建工件。这样,Truffle就会知道如何以合约可以理解的方式来格式化我们的函数调用。

让我们看一个简单的例子。

假设有一个合同叫做myAwesomeContract。我们可以执行以下操作来加载构建工件:

1
const myAwesomeContract = artifacts.require(“myAwesomeContract”);

该函数返回称为合同抽象的\东西。简而言之,合同抽象隐藏了与以太坊进行交互的复杂性,并为我们的Solidity智能合同提供了便捷的JavaScript接口。我们将在下一章中使用它。

contract()函数

在幕后,TruffleMocha周围添加了一个薄包装纸,以简化测试。由于我们的课程专注于以太坊开发,因此我们不会花太多时间来解释Mocha的位和字节。如果您想进一步了解Mocha,请在完成本课程后访问其网站。现在,您只需要了解我们在这里介绍的内容-如何:

  • 通过调用名为的函数对测试进行分组contract()。它通过提供用于测试并进行一些清理的帐户列表来扩展Mocha的功能。describe()

    contract()有两个参数。第一个是string,必须指出我们要测试的内容。第二个参数 callback是我们实际编写测试的地方。

  • 执行它们:我们将执行此操作的方法是调用一个名为named的函数it(),该函数也带有两个参数:a string描述测试的实际作用,描述callback

放在一起,这是一个简单的测试:

1
2
3
4
contract("MyAwesomeContract", (accounts) => {
it("should be able to receive Ethers", () => {
})
})

注意:经过深思熟虑的测试可以解释代码的实际作用。确保对测试套件和测试用例的描述可以作为一致的语句一起阅读。就像您在编写文档一样。

您要编写的每个测试都遵循这种简单的模式。很简单,不是吗?😁

进行测试

现在我们已经创建了一个空CryptoZombies.js文件,让我们填写它。

  1. 代码的第一行应声明一个const类型的变量,名为CryptoZombies,并将其设置为等于artifacts.require函数的结果,并以我们要测试的合同名称作为参数。

  2. 接下来,继续并从上方复制/粘贴测试。

  3. 更改我们的调用方式contract(),以使第一个参数为智能合约的名称。

    注意:不用担心accounts参数。我们将在下一章中对其进行解释。

  4. 传递给it()函数的第一个参数(在我们的示例中,就是“应该能够接收以太坊”)应该是测试的名称。由于我们将从创建新的僵尸开始,因此请确保将第一个参数设置为“应该能够创建新的僵尸”。

我们都准备好了。让我们进入下一章。

测试

这里给出的测试是用JavaScript语言对solidity进行测试,也可以采用soliditysolidity进行测试。等我学会了再说吧😂。

test/CryptoZombies.js

1
2
3
4
5
6
7
8
const CryptoZombies = artifacts.require("CryptoZombies");
//使用 artifacts.require 语句来取得准备部署的合约

contract("CryptoZombies", (accounts) => {
it("should be able to create a new zombie", () => {

})
})

第一个测试-创建一个新的僵尸

在部署到以太坊之前,最好在本地测试您的智能合约。

您可以使用名为Ganache的工具来进行此操作,该工具可以设置本地以太坊网络。

Ganache每次启动时,都会创建10个测试帐户,并为它们提供100个以太币,以使测试更加容易。由于GanacheTruffle紧密集成在一起,我们可以通过accounts上一章中提到的数组访问这些帐户。

但是使用accounts[0]accounts[1]不会使我们的测试读得好对吗?

为了帮助理解,我们想使用两个占位符名称-Alice和Bob。因此,在contract()函数内部,让我们像这样初始化它们:

1
let [alice, bob] = accounts;

注意:请宽恕较差的语法。在JavaScript中,约定是对变量名使用小写字母。

为什么选择爱丽丝和鲍勃?有一个古老的传统,使Alice和Bob或“ A和B”成为密码学,物理学,程序设计等中的通用名称。这是一段简短但有趣的历史,在您完成本课程后,非常值得一

现在,让我们继续第一个测试。

创建一个新的僵尸

说爱丽丝想玩我们很棒的游戏。如果是这样,她要做的第一件事就是创建自己的僵尸🧟。为此,前端(在本例中为Truffle)将必须调用该createRandomZombie函数。

注意:作为回顾,以下是我们合同中的Solidity代码:

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

我们首先测试此功能。

进行测试

  1. contract()函数的第一行应声明两个名为的变量alicebob然后如上所示初始化它们。
  2. 接下来,我们要适当地调用该it()函数。第二个参数(一个callback函数)将与区块链“对话”,这意味着该函数是异步的。只需在async关键字前面加上。这样,每次使用await关键字调用此函数时,我们的测试就会等待它返回。

解释承诺如何工作超出了本课程的范围。学完本课程后,请随时查看官方文档以进一步了解。

测试

test/CryptoZombies.js

1
2
3
4
5
6
7
8
const CryptoZombies = artifacts.require("CryptoZombies");
contract("CryptoZombies", (accounts) => {
//1. 初始化 `alice` 和 `bob`
let [alice, bob] = accounts;
it("should be able to create a new zombie", async () => {
//2 & 3. 替换第一个参数并使回调异步
})
})

第一次测试-创建新的僵尸(续)

很好!既然我们已经有了第一个测试的外壳,那么让我向您介绍测试的工作原理。

通常,每个测试都有以下阶段:

  1. set up:我们定义初始状态并初始化输入。
  2. act:我们实际在哪里测试代码。始终确保只测试一件事
  3. assert:我们在哪里检查结果。

让我们更详细地看看我们的测试应该做什么。

1.set up

在第2章中,您学习了如何创建合同抽象。但是,顾名思义,合同抽象只是一种抽象。为了与智能合约进行实际交互,我们必须创建一个JavaScript对象,该对象将作为合约的实例。继续我们的示例myAwesomeContract,我们可以使用契约抽象来初始化我们的实例,如下所示:

1
const contractInstance = await myAwesomeContract.new();

好,接下来该怎么办?

调用createRandomZombie要求我们将僵尸的名称作为参数传递给它。因此,下一步将是给爱丽丝的僵尸起个名字。诸如“爱丽丝的真棒僵尸”之类的东西。

但是,如果我们对每个测试都这样做,那么我们的代码将看起来不那么漂亮。更好的方法是如下初始化全局数组:

1
const zombieNames = ["Zombie #1", "Zombie #2"];

然后,调用合同的方法,如下所示:

1
contractInstance.createRandomZombie(zombieNames[0]);

注意:例如,如果要编写一个可以创建1000个僵尸而不是一个或两个的僵尸的测试,使用数组存储僵尸的名称将非常方便。

进行测试

我们已经zombieNames为您初始化了数组。

  1. 让我们创建一个合同实例。声明一个const名为的新名称contractInstance,并将其设置为等于CryptoZombies.new()函数的结果。
  2. CryptoZombies.new()与区块链“对话”。这意味着它是一个异步函数。让我们await在函数调用之前添加关键字。

测试

test/CryptoZombies.js

1
2
3
4
5
6
7
8
9
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();
})
})

第一个测试-创建一个新的僵尸(续)

现在我们已经排好了鸭-僵尸僵尸-让我们前进到下一个阶段… 🧟🦆‍🧟🦆🧟🦆‍🧟🦆🧟🦆‍🧟🦆

2.act

我们已经到达要调用为Alice-创建新僵尸的函数的部分createRandomZombie

但是有一个小问题-我们怎么做才能让方法“知道”谁调用它?另一种表达方式是-我们如何确保爱丽丝(而不是鲍勃)将成为这个新僵尸的所有者?🧐

嗯…问题可以通过合同抽象解决。Truffle的功能之一是它包装了原始的Solidity实现,并让我们通过传递该地址作为参数来指定进行函数调用的地址。

以下调用createRandomZombie并确保msg.sender将其设置为爱丽丝的地址:

1
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});

现在,我有一个简单的问题要问:您知道存储在其中的内容result吗?

好吧,让我解释一下。

日志和事件

一旦指定了要使用进行测试的合同artifacts.requireTruffle 便会自动提供由智能合同生成的日志。这意味着我们现在可以name使用类似以下内容来检索Alice的新创建的僵尸:result.logs[0].args.name。以类似的方式,我们可以得到id_dna

除了这些信息之外,result还将为我们提供有关交易的其他一些有用的细节:

  • result.tx:交易哈希
  • result.receipt:包含交易收据的对象。如果result.receipt.status等于,true则表示交易成功。否则,意味着事务失败。

注意:请注意,日志也可以用作存储数据的便宜得多的选项。缺点是无法从智能合约本身内部对其进行访问。

3.assert

在本章中,我们将使用内置的断言模块,该模块带有一组断言函数,例如equal()deepEqual()。简而言之,throw如果结果与预期不符,这些功能将检查条件和错误。由于我们将比较简单的值,因此我们将开始运行assert.equal()

进行测试

让我们结束第一个测试。

  1. 声明一个const名为 result,并将其设置为等于以contractInstance.createRandomZombie僵尸的名称(zombieNames[0],)和所有者{from: alice}作为参数的结果。
  2. 一旦有了result,请assert.equal使用两个参数-result.receipt.status和进行调用true

如果上述条件成立,我们可以假设我们的测试已通过。为了安全起见,我们在这里时再添加一张支票。

  1. 在下一行中,检查是否result.logs[0].args.name等于zombieNames[0]。使用assert.equal,就像我们上面所做的一样。

现在,该运行truffle test并查看我们的第一个测试是否通过了。这种工作方式是Truffle只会检查“ test”目录并执行在该目录中找到的文件。

实际上,我们已经为您做到了。输出应如下所示:

1
2
3
4
5
Contract: CryptoZombies
✓ should be able to create a new zombie (323ms)


1 passing (768ms)

至此,您的第一个测试结束了-做得好!还有更多其他内容,所以让我们继续下一课…

测试

test/CruptoZombies.js

1
2
3
4
5
6
7
8
9
10
11
12
13
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]);

})
})

在游戏中保持乐趣

到目前为止很棒的工作!现在我们可以确定用户可以创建新的僵尸了。

但是,如果他们可以继续调用此功能以在其军队中创建无限的僵尸,则游戏将不会很有趣。因此,在第2课的第4章中,我们向添加了一条require语句,以createZombieFunction()确保每个用户不能拥有一个以上的僵尸:

1
require(ownerZombieCount[msg.sender] == 0)

让我们测试一下此功能,看看它是否有效。

钩子(hooks)

在短短几分钟内🤞,我们将进行多个测试,而工作方式是每个测试都应以一张干净的纸开始。因此,对于每个测试,我们都必须创建一个智能合约的新实例,如下所示:

1
const contractInstance = await CryptoZombies.new();

如果您可以只编写一次并让Truffle在每次测试中都自动运行它,那会不会很好?

好吧… Mocha(和Truffle)的功能之一是能够在测试之前或之后运行一些称为钩子的代码片段。要在执行测试之前运行某些程序,应将代码放在名为函数中beforeEach()

因此,contract.new()您不必像这样写几次,只需这样做一次:

1
2
3
4
beforeEach(async () => {
// let's put here the code that creates a new contract instance
//让我们将创建新合约实例的代码放在这里
});

然后,Truffle会照顾好一切。真是太好了,不是吗?

进行测试

  1. 在初始化alice和bob的代码行下方,我们声明一个名为的变量contractInstance。不要将其分配给任何东西。

    注意:我们希望contractInstance将范围限制为定义它的块。使用let代替var

  2. 接下来,从上方复制/粘贴代码段以定义beforeEach()功能。

  3. 让我们填写新函数的主体。继续并移动beforeEach()函数内部创建新合同实例的代码行。现在我们在contractInstance其他地方定义了,您可以删除const限定符。

  4. 我们将需要一个it用于测试的新空函数。将测试名称(这是我们传递给it函数的第一个参数)设置为“不应允许两个僵尸”。

我们将在下一章继续充实此功能!


be‍♂️这里是…各种各样的僵尸!!! 🧟‍♂️

如果您确实非常想精通掌握\,请继续阅读。否则,只需单击下一步,然后转到下一章。

你还在吗?

太棒了!毕竟,您为什么要否认自己很多很棒的事情?

现在,让我们回到contract.new工作原理上来。基本上,每次我们调用此函数时,Truffle都会使它生效,以便部署新合同。

一方面,这很有用,因为它使我们可以使用干净的表开始每个测试。

另一方面,如果每个人都将创建无数合同,则区块链将变得肿。我们希望您随处逛逛,但不要您的旧测试合同!

我们想防止这种情况发生,对吧?

令人高兴的是,解决方案非常简单…… selfdestruct一旦不再需要我们的合同。

其工作方式如下:

  • 首先,我们希望CryptoZombies像这样向智能合约添加新功能:

    1
    2
    3
    function kill() public onlyOwner {
    selfdestruct(owner());
    }

    注意:如果您想了解更多信息selfdestruct(),可以在此处阅读Solidity文档 。要记住的最重要的事情是,将特定地址的代码从区块链中删除的唯一方法。这使其成为非常重要的功能!selfdestruct

  • 接下来,与beforeEach()上面解释的功能有些相似,我们将创建一个名为afterEach()

    1
    2
    3
    afterEach(async () => {
    await contractInstance.kill();
    });
  • 最后松露将确保在执行测试后调用此函数。

瞧,智能合约将其移除!

在本课程中,我们有很多基础要讨论,而实现此功能可能至少需要增加2章。因此,我们相信您可以添加它。💪🏻

测试

test/CruptoZombies.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
let [alice, bob] = accounts;
// start here
let contractInstance;
beforeEach(async () => {
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 () => {

})


})

在游戏中保持乐趣(续)

在本章中,我们将填写第二个测试的正文。这是应该做的:

  • 首先,爱丽丝应该打电话createRandomZombie给它zombieNames[0],并将其作为她的第一个僵尸的名字。
  • 接下来,爱丽丝应该尝试创建她的第二个僵尸。唯一不同的是,这次,僵尸名称应设置为zombieNames[1]
  • 在这一点上,我们期望合同throw有误。
  • 由于仅当智能合约出错时我们的测试才能通过,因此我们的逻辑看起来会有所不同。我们必须将第二个createRandomZombie函数调用包装在一个try/catch块内,如下所示:
1
2
3
4
5
6
7
8
9
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”,如下所示:

1
const utils = require("./helpers/utils");

这就是调用该函数的代码行的样子:

1
await utils.shouldThrow(myAwesomeContractInstance.myAwesomeFunction());

进行测试

在上一章中,我们为第二个测试创建了一个空外壳。让我们填写它。

  1. 首先,让爱丽丝创建她的第一个僵尸。给它zombieNames[0]起名字,不要忘记正确设置所有者。
  2. 爱丽丝创建第一个僵尸之后,shouldThrow使用createRandomZombie作为参数运行。如果您不记得执行此操作的语法,请从上面检查示例。但是首先,请尝试不偷看。

太棒了,您刚刚编写完第二个测试!

现在,我们已经开始truffle test为您服务。这是输出:

1
2
3
4
5
6
Contract: CryptoZombies
✓ should be able to create a new zombie (129ms)
✓ should not allow two zombies (148ms)


2 passing (1s)

测试通过了。万岁!

测试

test/CruptoZombies.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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}));

})
})

Zombie Transfers

问题-说爱丽丝想把僵尸送给鲍勃。我们可以测试一下吗?

当然!

如果您一直遵循前面的课程,则应该知道,除其他外,我们的僵尸继承自ERC721。而ERC721规范有两种不同的方式来传输令牌:

(1)

1
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

第一种方法是让爱丽丝(所有者)呼叫transferFromaddress作为_from参数,鲍勃 address作为_to参数,然后zombieId她想转移。

(2)

1
function approve(address _approved, uint256 _tokenId) external payable;

其次是

1
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

第二种方法是让Alice首先approve用Bob的地址和呼叫zombieId。合同然后存储鲍勃被批准采取僵尸。接下来,当Alice或Bob致电时transferFrom,合同检查该地址msg.sender是否等于Alice或Bob的地址。如果是这样,它将僵尸转移给Bob。

我们将这两种转移僵尸的方式称为“方案”。为了测试每种情况,我们希望创建两个不同的测试组并为其提供有意义的描述。

为什么要分组?我们只有一些测试…

是的,现在我们的逻辑非常简单,但这可能并非总是如此。不过,第二种情况(approve后跟transferFrom)至少需要进行两项测试:

  • 首先,我们必须检查爱丽丝本人是否能够转移僵尸。
  • 其次,我们还必须检查Bob是否被允许运行transferFrom

此外,将来,您可能希望添加其他功能,这些功能需要进行不同的测试。我们认为最好从一开始就将可伸缩的结构放在适当的位置。如果您花了一些时间专注于其他事情,那么对于外部人员或您自己来说,了解代码将变得更加容易。

注意:如果您最终处于与其他编码人员一起工作的位置,则会发现他们更有可能遵循您在初始代码中规定的任何约定。如果您想从事大型,成功的项目,那么有效地协作是您需要的关键技能之一。养成良好的习惯,尽早地帮助您做到这一点,将使您作为一名编码员的生活更轻松,更成功。

上下文功能

为了对测试进行分组,松露提供了一个名为的功能context。让我快速向您展示如何使用它,以更好地构建我们的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 transferForm", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferForm", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})

如果我们将其添加到CryptoZombies.js文件中然后运行truffle test,输出将类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
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 transferForm
✓ should approve and then transfer a zombie when the approved address calls transferForm


5 passing (2s)

好?

嗯…

再看一看-以上输出存在问题。看来所有测试都通过了,这显然是错误的,因为我们甚至还没有编写它们!

幸运的是,有一个简单的解决方案-如果我们仅将a x放在context()函数前面,如下所示:xcontext()Truffle将跳过这些测试。

注意:x也可以放置在it()功能的前面。编写完这些功能的测试后,别忘了删除所有x!

现在,让我们开始吧truffle test。输出应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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 transferForm
- should approve and then transfer a zombie when the approved address calls transferForm


2 passing (827ms)
3 pending

其中“-”表示已被该“ x”标记跳过的测试。

很整洁吧?现在,您可以继续进行测试,并标记出空白的函数,以备不时之需。

进行测试

  1. 继续并从上面复制/粘贴代码。
  2. 现在,让我们跳过我们的新context功能。

我们的测试只是空壳,要实现它们,需要编写很多逻辑。在接下来的章节中,我们将以较小的部分来做。

测试

test/CruptoZombies.js

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}));
})

// start herex
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 transferForm", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferForm", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})

})

ERC721令牌转移-单步方案

到目前为止,我们只是在热身…

但是现在是时候真正炫耀您所知道的!

在下一章中,我们将把我们学到的东西放在一起,并测试一些很酷的东西。

首先,我们将一步一步测试Alice将她的ERC721令牌转移给Bob的场景。

这是我们的测试应执行的操作:

  • 为爱丽丝创建一个新的僵尸(记住,僵尸不过是ERC721令牌)。
  • 做到这一点,以便Alice将她的ERC721令牌转让给Bob。
  • 此时,Bob应该拥有ERC721令牌。如果是这样,ownerOf将返回一个等于鲍勃地址的值。
  • 让我们结束它通过检查Bob是newOwner,里面的assert

进行测试

  1. 函数的第一行应调用createRandomZombie。为其zombieNames[0]命名,并确保Alice是所有者。

  2. 第二行应声明一个constnamed zombieId,并将其设置为等于僵尸的id。在第4章中,您学习了如何检索此信息。如有必要,请刷新您的内存。

  3. 然后,我们必须transferFrom使用alicebob作为第一个参数进行调用。确保Alice调用了此函数,并且await在继续下一步之前,我们希望它能够完成运行。

  4. 声明一个const被叫newOwner。将其设置为ownerOf与一起调用zombieId

  5. 最后,让我们检查Bob是否拥有此ERC721令牌。将其放入代码中,意味着我们应该assert.equal使用newOwnerbob作为参数运行;

    注意:assert.equal(newOwner, bob)assert.equal(bob, newOwner)基本上是同一回事。但是我们的命令行解释器不是太高级,因此除非您键入第一个选项,否则它将不会认为您的答案正确。

  6. 我是否说上一步是最后一步!好吧…这是个谎言。我们要做的最后一件事是通过删除来“跳过”第一种情况x

!那是很多代码。希望您能正确解决。如果没有,请随时单击“显示答案”。

现在运行truffle test,看看我们的新测试是否通过:

1
2
3
4
5
6
7
8
9
10
11
12
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 transferForm
- should approve and then transfer a zombie when the approved address calls transferForm


3 passing (1s)
2 pending

在那里!我们的代码以出色的表现通过了测试👏🏻。

在下一章中,我们将继续进行两步方案,approve其后是transferFrom

测试

test/CruptoZombies.js

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 () => {
// 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 transferForm", async () => {
// TODO: Test the two-step scenario. The approved address calls transferFrom
})
it("should approve and then transfer a zombie when the owner calls transferForm", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
})

ERC721代币转移-两步方案

现在,approve接下来的transferFrom转移ERC721令牌的方法远非在公园散步,但我在这里为您提供帮助。

简而言之,我们必须测试两种不同的情况:

  • 爱丽丝批准鲍勃使用ERC721令牌。然后,鲍勃(批准的地址)致电transferFrom
  • 爱丽丝批准鲍勃使用ERC721令牌。接下来,爱丽丝转移ERC721令牌。

在这两种情况的区别在于调用实际转移,甲和乙。

我们让它看起来很简单,对吧?

让我们看一下第一种情况。

鲍勃调用transferFrom函数

此方案的步骤如下:

  • 爱丽丝创建一个新的ERC721令牌,然后呼叫approve
  • 接下来,Bob运行transferFrom,这应该使他成为EC721令牌的所有者。
  • 最后,我们必须assert.equal使用newOwnerbob作为参数进行调用。

进行测试

  1. 我们测试的前两行代码与之前的测试相似。我们已继续为您复制粘贴。
  2. 接下来,为了让Bob批准使用ERC721令牌,请调用approve()。该函数以bobzombieId`作为参数。另外,请确保Alice调用了该方法(因为将传输她的ERC721令牌)。
  3. 最后三行代码几乎与之前的测试相似。同样,我们已继续为您复制粘贴它们。让我们更新transferFrom()函数调用,以使发送者为Bob。
  4. 最后,让我们“跳过”这种情况,“跳过”最后一个测试用例,我们仍然要编写一个测试用例。

是时候运行truffle test,看看我们的测试是否通过了:

1
2
3
4
5
6
7
8
9
10
11
12
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 transferForm (360ms)
- should approve and then transfer a zombie when the approved address calls transferForm


4 passing (2s)
1 pending

测试

test/CruptoZombies.js

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
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 transferForm", 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 transferForm", async () => {
// TODO: Test the two-step scenario. The owner calls transferFrom
})
})
})

ERC721代币转移-两步方案(续)

我们即将完成对转移的测试!现在让我们测试Alice调用的场景transferFrom

我们为您带来一些好消息-此测试非常简单。您要做的就是复制并粘贴上一章中的代码,并进行编写,以便Alice(而非Bob)调用transferFrom

进行测试

  1. 复制并粘贴上一个测试中的代码,并进行Alice调用transferFrom
  2. “跳过”它,我们都准备好了。

如果运行truffle test,输出将类似于以下内容:

1
2
3
4
5
6
7
8
9
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 transferForm (266ms)
5 passing (3s)

我想不出与传输相关的其他任何测试,因此我们现在就完成了。

测试

test/CruptoZombies.js

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.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 () => {
// TODO: start
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);
})
})
})

Zombie Attacks

哇!前面的章节是非常密集的信息,但是我们涵盖了很多基础。

那么我们现在完成所有方案了吗?不,我们还没有到那儿。我们把最好的东西留到了最后。

我们已经建立了一个僵尸游戏,最好的部分是僵尸之间相互搏斗,对吧?

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

  • 首先,我们将创建两个新的僵尸-一个用于爱丽丝,另一个用于Bob。
  • 其次,爱丽丝(Alice)将以attack鲍勃(Bob’s)zombieId作为参数运行她的僵尸
  • 最后,为了通过测试,我们将检查是否result.receipt.status等于true

假设我们在这里,我已经对所有这些逻辑进行了快速编码,将其包装在一个it()函数中,然后运行truffle test

然后,输出将如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()

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

到目前为止,一切都很好。继续前进,让我们深入探讨_createZombie()

1
2
3
4
5
6
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块,这意味着,当我们将僵尸进行战斗时,智能合约将“看到”一天过去了。

放在一起,我们可以通过以下方式来修正测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

是的,还不够好。毕竟,我们真的希望您知道一天中头顶有几秒钟吗?

当然不是。这就是为什么我们添加了另一个名为helper的函数的原因days,该函数占用了我们想要增加时间的天数作为参数。您可以这样调用此函数:await time.increase(time.duration.days(1))

注意:显然,主网上或矿工保护的任何可用测试链上都没有时间旅行。如果任何人都可以选择改变时间在现实世界中的运行方式,那将是一团糟。为了测试智能合约,时间旅行可能是编码员的基本组成部分。

进行测试

我们继续进行并填写了失败的测试版本。

  1. 向下滚动到我们为您留下的评论。接下来,通过await time.increase如上所述运行来修复测试用例。

我们都准备好了。让我们运行truffle test

1
2
3
4
5
6
7
8
9
10
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)

然后你去!这是本章的最后一步。

测试

test/CruptoZombies.js

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
59
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 time.increase(time.duration.days(1));

await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
assert.equal(result.receipt.status, true);
})
})

与chai的更多表达断言

到目前为止,我们一直在使用内置assert模块来编写我们的断言。虽然还不错,但该assert模块有一个主要缺点-代码读取效果不佳。幸运的是,那里有几个更好的断言模块,并且Chai是最好的之一。

chai断言图书馆

Chai是非常强大的功能,在本课程的范围内,我们将只涉及一些问题。学完本课程后,请随时查看他们的指南以进一步了解您的知识。

也就是说,让我们看看捆绑到的三种断言样式Chai

  • Expect:让您链接自然语言断言,如下所示:

    1
    2
    let lessonTitle = "Testing Smart Contracts with Truffle";
    expect(lessonTitle).to.be.a("string");
  • 应该:允许与expect接口类似的断言,但是链以一个should属性开始:

    1
    2
    let lessonTitle = "Testing Smart Contracts with Truffle";
    lessonTitle.should.be.a("string");
  • assert:提供与node.js打包的符号类似的符号,并包括其他一些测试,并且与浏览器兼容:

    1
    2
    let lessonTitle = "Testing Smart Contracts with Truffle";
    assert.typeOf(lessonTitle, "string");

在本章中,我们将向您展示如何使用改善断言expect

注意:我们假设该chai软件包已安装在您的计算机上。如果不是这种情况,您可以像这样轻松地安装它:npm -g install chai

为了使用expect样式,我们首先要做的就是将其导入到我们的项目中,如下所示:

1
var expect = require('chai').expect;

Expect().to.equal()

现在,我们已经将导入expect到我们的项目中,检查两个字符串是否相等如下所示:

1
2
let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');

聊够了。让我们的Chai权力得到充分利用!

进行测试

  1. 导入expect我们的项目。
  2. 继续上面的示例zombieName,我们可以使用expect来测试是否成功完成事务,如下所示:
1
expect(result.receipt.status).to.equal(true);

我们可以检查爱丽丝是否拥有这样的僵尸:

1
expect(zombieOwner).to.equal(alice);
  1. 用替换所有出现assert.equalexpect。我们在代码中留下了很多注释,以使其易于查找。

测试

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
59
60
61
62
63
64
65
66
67
68
69
70
71
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
//assert.equal(result.receipt.status, true);
expect(result.receipt.status).to.equal(true);
//assert.equal(result.logs[0].args.name,zombieNames[0]);
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.toNumber();
await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
const newOwner = await contractInstance.ownerOf(zombieId);
//TODO: replace with expect
//expect(zombieOwner).to.equal(alice);
//assert.equal(newOwner, bob);
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 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);
//TODO: replace with expect
//assert.equal(newOwner,bob);
expect(newOwner).to.equal(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);
//TODO: replace with expect
//assert.equal(newOwner,bob);
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
//assert.equal(result.receipt.status, true);
expect(result.receipt.status).to.equal(true);
})
})

Testing Against Loom

令人印象深刻!你一定一直在练习。

现在,如果不向您展示如何针对Loom\ Testnet 进行测试,本教程将是不完整的。

回想一下我们以前的课程,在 Loom上\,与以太坊相比,用户可以更快,更省油地进行交易。这使得DAppChains更适合于游戏或面向用户的DApp。

你知道吗?针对Loom进行部署和测试完全没有什么不同。我们已经进行了总结,总结了需要做的事情,以便您可以对Loom\进行测试。让我们快速看一下。

配置松露在织布机上测试

首先是第一件事。让我们通过在networks对象内部放置以下代码片段来告诉Truffle如何部署到Loom Testnet 。

1
2
3
4
5
6
7
8
9
10
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,任何人都可以看到它。

帐户数组

为了使TruffleLoom进行对话,我们已将默认值替换为HDWalletProvider我们自己的Truffle Provider。结果,我们必须告诉我们的提供者填写accounts数组,以便我们可以测试游戏。为此,我们需要替换returnsa new 的代码行LoomTruffleProvider

1
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)

有了这个:

1
2
3
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;

进行测试

  1. 用上面的代码段替换return新代码行LoomTruffleProvider

我们还要注意一件事。仅在针对Ganache进行测试时才可以使用时间旅行,因此我们应该跳过此测试。您已经知道如何通过在函数名称前放置一个来跳过测试x。但是,这次我们希望您学习一些新知识。长话短说…您可以通过简单地链接函数调用来跳过测试,skip()如下所示:

1
2
3
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

如果从上方输入命令,则输出应类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 transferForm (22388ms)


5 passing (2m)
1 pending

伙计们,到此为止!我们已经完成了对CryptoZombies智能合约的测试。

测试

truffle.js

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
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
//return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;
},
network_id: '9545242630824'
}
},
compilers: {
solc: {
version: "0.4.25"
}
}
};

image-20200702102922238

您的支持是我继续创作的动力