Lesson2:用Truffle测试智能合约

欢迎!通过完成前面的课程,你已经证明了你真正了解你的东西。
因此,继续将游戏部署到主网。享受你的成功!
等一下……有一些事情你可能已经想到了。毕竟,一旦合约被部署到主网,它们将永远存在。如果其中有错误,它们也会继续存在。就像不死僵尸。
无论多么熟练,每个程序员都会遇到错误或bug。你不太可能犯一个大错误,比如给攻击僵尸100%的胜利机会,但这是可能的。

显然,给予进攻方100%的胜率将意味着你所写的内容不再是游戏,甚至没有任何乐趣。这样的漏洞可能会让你的游戏彻底失败,也不可能让你的僵尸死里逃生。
为了防止这种可怕的事情发生,你必须彻底测试游戏的每个方面。
本课结束时,你将能够:

  • TruffleGanache测试你的智能合约
  • 使用Chai编写更具表现力的断言
  • Loom测试😉

我们开始吧

Chapter 1: 准备就绪

在本课中,我们将介绍测试以太坊智能合约背后的理论,重点是Truffle,GanacheChai。你将需要一个中等水平的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。

实战演习

最佳实践是为每个合约创建一个单独的测试文件,并为其指定智能合约的名称。这使得在长期运行中更容易管理测试,特别是当项目增长和更改时。

  1. 在右侧终端运行`请添加图片描述

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文件,让我们来填充它。

  1. 第一行代码应该声明一个名为CryptoZombiesconst,给他赋值为artifacts.require的结果,将我们要测试的合约的名称作为参数。
  2. 接下来,从上面复制/粘贴测试。
  3. 修改调用contract()的方式,使第一个参数是我们的智能合约的名称。

注意:不要担心accounts。我们将在下一章解释它。

  1. 传递给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);
 }

我们从测试这个函数开始

实战演习

  1. contract()函数的第一行应该声明两个变量alice和bob,并如上所示对它们进行初始化。
  2. 接下来,我们想要正确地调用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,让我向您介绍测试是如何工作的。
通常,每个测试都有以下几个阶段:

  1. set up:其中我们定义初始状态并初始化输入。
  2. act:我们实际测试代码的地方。一定要确保你只测试一件事。
  3. 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数组。

  1. 让我们创建一个合约的实例。声明一个名为contractInstance的新const,并将其设置为与CryptoZombies.new()函数的结果相等。
  2. 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()

实战演习

让我们完成第一个测试。

  1. 声明一个名为result的const,并将其设置为contractInstance的结果。createRandomZombie,使用僵尸的名字和所有者作为参数。

  2. 得到结果后,调用assert。等于两个参数- result. receive .status和true。
    如果上述条件成立,我们可以假设我们的测试已经通过。为了安全起见,我们在这里再加一个检查。

  3. 在下一行中,检查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会照顾好一切。真贴心,不是吗?

实战演习

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

注意:我们希望将contractInstance的作用域限制在定义它的块中。使用let代替var。

  1. 接下来,复制/粘贴上面用于定义beforeEach()函数的代码片段。
  2. 让我们来填充新函数的函数体。继续移动在beforeEach()函数中创建新契约实例的代码行。既然我们已经在别处定义了contractInstance,就可以删除const限定符了。
  3. 我们需要一个新的空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。我们把它填上。

  1. 首先,让爱丽丝创造她的第一个僵尸。将其命名为zombieNames[0],不要忘记正确设置所有者。
  2. 在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”标记跳过的测试。
很整洁,是吧?现在您可以运行您的测试,并标记出您知道在不久的将来需要编写测试的空函数。

实战演习

  1. 继续复制/粘贴上面的代码。
  2. 现在,让我们跳过新的上下文函数。
    我们的测试只是空壳,为了实现它们需要编写大量的逻辑。在接下来的章节中,我们将以更小的部分来做。

代码更新

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来结束它。

实战演习

  1. 函数的第一行应该调用createRandomZombie。将其命名为zombieNames[0],并确保Alice是所有者。
  2. 第二行应该声明一个名为zombieId的const,并将其设置为僵尸的id。在第五章中,我们学习了如何从智能合约的日志和事件中检索信息。如果需要的话,刷新你的记忆。确保还使用tonnumber()将zombieId转换为一个有效的数字。
  3. 然后,我们必须以alice和bob作为第一个参数调用transferFrom。确保Alice调用了这个函数,我们等待它完成运行,然后移动到下一步。
  4. 声明一个名为newOwner的const。将其设置为与zombieId调用的ownerOf相等。
  5. 最后,让我们检查Bob是否拥有这个ERC721令牌。在代码中,这意味着我们应该运行assert。用newOwner和bob作为参数;

注意:assert.equal(newOwner, bob) 和assert.equal(bob, newOwner)基本上是一样的。但是我们的命令行解释器不是很高级,所以它不会认为你的答案是正确的,除非你输入第一个选项。

  1. 我说过前一步是最后一步吗?嗯…这是个谎言。我们要做的最后一件事是通过删除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 。用newOwnerbob作为参数。

实战演习

  1. 我们测试的前两行代码与前面的测试类似。我们已经复制粘贴好了。
  2. 接下来,为了批准Bob使用ERC721 Token,调用approve()。该函数以bob和zombieId作为参数。另外,确保Alice调用了该方法(因为要传输的是她的ERC721 Token)。
  3. 最后三行代码几乎与前面的测试相似。同样,我们已经为你们复制粘贴了它们。让我们更新transferFrom()函数调用,使发送方为Bob。
  4. 最后,让我们“取消跳过”这个场景并“跳过”那个场景

现在运行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;

实战演习

  1. 用上面的代码片段替换返回新的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。我们强烈建议您从查看我们的文档开始。

  • 44
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值