跳转至

第四天-创建测试

您一直依赖测试来确保您的代码正确运行,因此您可能对它们的重要性以及它们如何帮助您编写错误更少的更好代码有所了解。在这一章中,我们将看看如何创建适当的测试,可以帮助我们的游戏顺利运行,没有任何问题。本章将涵盖以下主题:

  • 理解单元测试和集成测试
  • 各种应用的测试策略
  • 在 Solidity 中创建单元测试
  • 在测试中对同一功能进行多次测试
  • 用 JavaScript 创建集成测试
  • 运行测试套件

理解单元测试和集成测试

今天,我们将谈论所有关于考试的话题。在过去的几天里,你实际上已经体验到了测试的一些好处。我编写了一个测试来检查我们的智能契约中的特定行为,但它失败了,然后您编写了一个代码,并且您知道当测试通过时它会交付预期的结果。

为什么要写测试?

现在,想象一下在更大的范围内。你正在作为一个庞大的区块链开发团队的一部分编写代码,这个团队正在向全世界发布最新的去中心化应用程序。作为团队的一员,您如何知道团队中其他开发人员的代码做了它应该做的事情?当一个 bug 出现时,你如何确保你的代码不是导致它的代码?或者这样说:如果有人更新了你的代码,你怎么能确定它还在做它应该做的事情?请记住,当我们处理区块链应用程序时,我们是在处理人们的钱,所以所有这些都是重要的问题,所有这些问题的答案都是一样的:测试。

单元测试

我们今天要讨论两种不同的测试,所以我想先给你们介绍一下,这样你们就能理解每种测试的作用。单元测试是由开发人员编写的,用来测试相对较小的一段代码,以确保它正在做它应该做的事情。想象一个将两个数相加的函数:

function sum(unit a, unit b) returns
(unit) {
return a + b;
}

测试可能看起来像这样:

function testAddCorrect() public {
     unit expected = 4;
     unit result = myContact.sum(1, 4);
     Assert.equal(expected, result);
}

它给函数两个数字,然后测试正确的结果。

以下是单元测试的一些常见特征:

  • 范围狭窄
  • 易于阅读和书写
  • 没有依赖关系

这意味着测试本身是完全独立的,不需要数据库、网络连接、人工干预,或者除了测试和被测试的代码之外的任何东西。在可靠性方面,我们没有真正的单元测试,因为即使是基本的测试也需要 Ganache 或本地区块链网络来运行。即便如此,我们仍然以这种方式编写测试,以确保代码的正常运行。让我们看一看我已经编写的一个单元测试,让您更好地了解真正的功能测试是什么样的:

在前面的截图中,左边是我们的determineWinner函数,您在过去几天里一直在使用它。右边是对它的测试。我们使用一个function关键字,给我们的测试起一个以小写的test开头的名字,然后在里面我们声明一个叫做expected的变量,这是我们期望这个函数正常工作时产生的结果,然后我们有另一个叫做result的变量,这是这个函数的实际结果。最后,我们使用一个断言或创建一个assert语句,比较预期结果或预期答案和实际结果,以确保它们相等,如果不相等,就会显示一条消息,让运行测试的人知道哪里出错了。

集成测试

另一方面,集成测试被用来证明系统的不同部分一起工作。为了更好地说明集成测试,让我向您展示我为作业编写的测试代码:

it('Should record player losses', async() => {
    const gameRound = await gaming.winOrLose(10, true, {
        from: player1,
        value: web3.utils.toWei('1', 'ether')
    })
    const playerStats = await gaming.players(player1)
    assert.equal(playerStats[1].toNumber(), 1, 'The player should have 1 toss')
})

这个测试是用 JavaScript 编写的。我们首先调用winOrLose函数,用所需的参数模拟 React 应用程序的调用。我们等待使用关键字await将该调用写入区块链。这个测试是检查球员的数据是否被正确记录。当他的代码完成时,玩家应该有一个记录的损失。因此,现在我们调用区块链并执行该函数来获取玩家的统计数据,并验证记录的损失值是否等于 1。

因此,在本次测试中,我们两次前往区块链,我们依赖区块链网络正常运行来通过测试。这是一个综合测试。以下是集成测试的一些共同特征。

他们证明了系统的不同部分可以协同工作。通常,它们覆盖整个应用程序,您可能也听说过端到端测试。它们需要比单元测试多得多的努力才能组合在一起,它们还需要外部资源,如数据库、硬件,或者在我们的例子中是区块链网络,它们更类似于我们的用户期望采取的行动。

在下一节中,您将学习如何使用 Solidity 和 JavaScript 创建单元测试和集成测试的基础知识。有了这些技能,你将能够创建测试来确保你的合同完全按照它应该做的去做。

各种应用的测试策略

您知道您应该编写测试,但是当您盯着空白屏幕时,有时很难知道从哪里开始。因此,在这一章中,我们将探索一些策略来帮助建立一个测试什么以及如何测试的计划。我们将讨论四件不同的事情:

  • 成功测试
  • 故障测试
  • 使用可靠性进行测试
  • 使用 JavaScript 进行测试

当谈到测试时,有许多不同的方法,但是与其淹没在信息的海洋中,我们要保持简单。任何测试都比没有测试好,所以让我们专注于对我们的合同进行一些测试,稍后我们可以随着了解的更多而不断完善方法和测试。

成功测试

最容易开始的地方是成功测试,我的意思是写测试来确保你的组件做它应该做的事情,当提供正确的输入时。考虑下面的代码片段:

function sum(uint a, uint b) returns
(uint) {
return a + b;
}

这里,我们有一个将两个数相加的函数。你如何检查操作是否完全按照它应该的那样工作?让我们写一个测试来确保如果给出两个数字,它会产生正确的答案。为此,我们将创建一个名为testAddCorrect()的函数,它考虑一个预期值和来自契约的结果值,并对它们进行交叉检查,以确保该函数产生正确的答案。下面的代码片段进一步说明了这个测试是如何工作的:

function testAddCorrect() public {
    uint expected = 4;
    uint result = myContract.sum(1, 4);
    Assert.equal(expected, result);
}

编写这些测试应该成为您开发工作流程中自然的一部分。事实上,有一种策略叫做测试驱动的开发,你先写测试,看到它失败,然后写代码让测试通过。一旦测试通过,您编写另一个失败的测试,然后使用代码使其通过。通过编写每个失败的测试,您可以确保代码做了它应该做的事情,然后通过每个测试,您可以专注于编写使您的应用程序工作所需的最少量的代码。这是我最常使用的策略,它非常有效,但是对于失败测试也有一些东西要说。

故障测试

如果你的函数在输入无效的情况下没有做正确的事情怎么办?考虑下面的 JavaScript 代码片段:

function sum(a, b) {
    return a + b;
}

这里,如果你给这个函数提供两个数字,它会把它们加起来,但是,如果我们给它两个字符串呢?

我们可能会要求它向调用者返回一个错误,指出只有数字是有效的输入,但是相反,它将返回两个连接在一起的输入字符串。这就是我所说的测试失败的意思。您需要一些额外的测试来涵盖当您的组件被给予无效输入时会发生什么。

大多数时候,这就是错误和安全漏洞的来源,通过以一种从未打算过的方式使用一个组件。

使用可靠性进行测试

我们的第三个主题是使用可靠性进行测试。如果这本书是你对编程世界的第一次介绍,这可能是你最舒服的地方。

在 Solidity 中编写测试与编写契约几乎是一样的,因为它是相同的编程语言,事实上,你的测试只是 Truffle 用来执行测试的另一个 Solidity 契约。用 Solidity 编写的测试类似于单元测试。每个测试套件或测试合同都在一个干净的房间环境中运行。这意味着,在每个测试套件运行之前,契约被重新部署到测试网络,因此您知道您是从一个已知的状态开始的。

因为每次运行测试时都会有一个部署,所以针对本地网络进行测试是有意义的,这也是我们使用 Ganache 的主要原因之一。如果我们必须部署到一个活动的网络,然后等待矿工挖掘每个事务,那么获得我们的测试结果将需要大量的时间,如果我们对自己诚实,我们不会像我们应该的那样频繁地运行我们的测试。

可靠性测试使用 Chai 断言库,它用于编写通过或未通过测试的逻辑。在接下来的章节在 Solidity 中创建单元测试中,您将会看到如何做到这一点。但是从测试的角度来看,Solidity 的功能非常有限,它非常适合测试单个函数,确保函数返回正确的响应,以及测试异常,但不太适合测试您的契约的整体行为。为此,我们将使用 JavaScript 测试。

使用 JavaScript 进行测试

JavaScript 测试为我们提供了一种方法来完全测试客户将要看到的契约行为。多亏了注入到测试运行程序中的web3提供者,我们可以访问测试账户,你将在第六章第六天:使用钱包中了解 web3 到底是什么。

Truffle 使用 Mocha 测试框架和 Chai 断言进行 JavaScript 测试。如果你以前写过 JavaScript,可能对 Mocha 比较熟悉;这里唯一的区别是松露用的是contract函数,而不是摩卡的described函数。这启用了前面提到的洁净室特性,以确保我们从每个测试套件的新合同开始。

使用 Chai 断言库是一个不错的尝试,因为它与我们在可靠性测试中使用的断言库是相同的,这使得我们作为区块链开发人员的生活稍微轻松了一些。现在,让我们深入到将合同部署到测试网络中。

在 Solidity 中创建单元测试

我们的第一个测试将会写得很扎实。从扎实开始可能会让你从一门你已经知道的语言开始熟悉这个外来的概念。

坚固性测试惯例

像 Solidity 中的许多东西一样,Solidity 测试也有一些约定:像文件这样的东西必须有.sol扩展名,契约名必须以大写字母 T 的单词Test开头,函数必须以小写字母 T 开头,并且test应该放在应用程序代码的test文件夹中。

要写一个测试,我们首先要做一点家务。我们定义合同,然后导入truffle/Assert.sol库:我们将导入truffle/DeployedAdresses.sol库。如果你习惯于编写节点应用程序,这两个可能会有点奇怪,因为通常这意味着库被导入,它是在node modules文件夹中找到的文件,但你不会在那里找到它,因为它是由 Truffle 直接导入的。我们还需要导入我们将要测试的合同。对于我们的应用程序,这是我们的游戏合同。这确实是您想要停止导入东西的地方,因为我们想要保持我们的测试最小化:

pragma solidity 0.5.0;
    import "truffle/Assert.sol";
    import "truffle/DeployedAddresses.sol";
    import "../contracts/Gaming.sol";

超过这一点导入额外的库只会引入复杂性和潜在的错误,所以就像我们以前编写的契约一样,我们将定义一个新的契约。因为是测试合同,所以我们要从名字测试开始。

我们现在可以做的一件很酷的事情是创建一个名为initialBalance的变量,并赋予它一些以太。当我们的合同得到部署时,它将获得此处指定的金额,使其可用于我们的合同。很酷,对吧?然后我们创建一个名为gaming的变量,它是我们的契约Gaming的实例,大写字母为 g。

contract TestGaming {
    uint public initialBalance = 10 ether;
    Gaming gaming;
}

我们可以定义一个名为beforeAll()的函数,注意它不是以单词test开头的,尽管我刚才说过函数必须以它开头。这是因为这是一个特殊的函数:它将在我们的测试套件中的任何测试之前运行。在其中,我们将获得已部署合同的实例,并将在其余测试中使用该实例:

function beforeAll() public {
    gaming = Gaming(DeployedAddresses.Gaming());
 }

我们可以使用的其他一些特殊功能有beforeEachafterAllafterEach。所以,现在,让我们开始写一些测试。我们的第一个测试将测试我们的determineWinner功能。它被winOrLose函数调用,但是我们将对它进行单独测试,以确保它确实做了它应该做的事情,这样我们就知道winOrLose函数可以指望它返回正确的响应。我们首先定义我们的函数,并以单词test开始给它命名,然后在我们的函数内部,我们将声明一个名为expected的变量。这是我们期望在测试中发现的结果。通过调用determineWinner函数来填充结果,我们向它传递一些执行函数所需的参数:

function testPlayerWonGuessHigher() public {
    bool expected = true;
    bool result = gaming.determineWinner(5, 4, true);
}

现在,我们的 Chai 断言库开始发挥作用了。我们调用assert库和equal函数,向其传递我们的expected值、结果和测试失败时我们将要显示的消息。现在,这条信息真的很重要:当这个测试失败时,这将是你或任何其他开发人员得到的唯一线索。确保它是清晰和具体的。这也是一个代码审查的好地方,因为获得其他人对这些消息的输入有助于使它们清晰易懂:

Assert.equal(expected, result, "The player should have won");

在测试中对同一功能进行多次测试

看看其他几个例子,除了我们刚刚写的测试,我们还有三个例子。在每个测试中,存在不同的参数变量,我们可以将这些变量提供给determineWinner函数,这允许我们检查我们可以从determineWinner函数中预期的每个可能的场景。这就是为什么可靠性测试人员是受欢迎的:他们非常容易编写,非常容易阅读,并且他们使用与我们的合同完全相同的编程语言。不幸的是,如果你尝试做更多的事情,乐趣和兴奋会很快消失:

为了进入下一个测试阶段,我们将使用 JavaScript。它有很多 Solidity 没有的功能。访问这些特性将为我们提供使用 JavaScript 进行端到端测试的能力和灵活性,这很方便,因为我们也将使用 JavaScript 编写我们的 UI。

用 JavaScript 创建集成测试

当用 JavaScript 编写测试时,我们不仅可以模拟契约调用,就像我们在 Truffle 测试中所做的那样,还可以选择不同的帐户、检查余额等等。让我们仔细研究一些测试,你会对我的意思有更好的理解。我们将从创建一个新的测试文件开始。对于我们的 Truffle 测试,文件名以字母 T 开头的单词test开始,以扩展名.sol结束。

我们的 JavaScript 测试以被测试的契约的名称开始,后面是带有大写字母 T 的单词 test,以扩展名.js结束。它仍然和我们的可靠性测试放在同一个test文件夹中,这意味着不管测试是用哪种语言编写的,只有一个地方可以找到测试。

在我们的文件中,我们将创建一个与测试中的契约同名的常量,并且我们使用需要该契约内容的工件。从这里开始,如果你熟悉的话,我们的测试看起来很像摩卡测试;不过,我们将使用 Truffle 关键字 contract,而不是使用 scribe:

const Gaming = artifacts.require('./Gaming.sol')

这启用了 Truffle cleanroom 功能,这意味着每次该文件作为测试运行时,Truffle 框架都会向网络部署一个新的契约实例,确保我们从一个已知的状态开始:

contract('Gaming', async (accounts) => {

现在我要声明几个变量。gaming变量将代表我们已经部署到网络上的合同版本,然后我将创建两个常量,ownerplayer 1。每个函数都从一个名为accounts的数组中获取一个项目。accounts变量是通过 Truffle 框架免费提供的,数组中的项代表了应用启动时 Ganache 为我们创建的accounts,所以这个名为owner的变量被设置为 account 数组中的第一项,也就是你在查看 Ganache 时看到列出的第一个帐户,名为player 1的变量是第二个列出的帐户。这是非常强大的,因为访问这些帐户允许我们以这些帐户的身份采取行动,然后用 Ganache 检查以确保事情如我们所预期的那样发生。它允许我们测试一些功能,比如应该适用于特定帐户而不适用于其他帐户的功能,比如我们的 is owner功能:

 let gaming
 const owner = accounts[0]
 const player1 = accounts[1]

现在我们将有一个before函数,很像它在 Truffle 中的对应函数,这个函数将在这个文件中编写的任何测试之前运行。如果您有额外的测试文件,它们将被认为是单独的运行,这里的before函数将不适用:

before(async () => {
 gaming = await Gaming.deployed()
})

然后,我们将使用asyncawait从以太坊网络获取我们的合同的部署版本。所以,让我为你分解一个asyncawait,以防你以前没见过。假设我们有一行 JavaScript:它将使用我们导入的表示我们的契约的工件来获得部署在以太坊网络上的实际契约实例,但是一旦我们调用这个函数,JavaScript 的工作方式就意味着它认为它已经完成了,并从这里继续前进:它是异步的。所以,即使我们调用了部署的函数,但它没有返回值,JavaScript 还是继续前进了。在这个调用完成之前,gaming变量实际上是未定义的,当您试图弄清楚为什么这个变量有时有值,有时没有值时,这会引起很多麻烦:

gaming = Gaming.deployed()
const fundGame = gaming.fundGame()

因此,为了避免这种痛苦,我们使用asyncawait。它的工作方式是,我们在这里使用async关键字声明这个匿名函数,然后在函数内部,每当我们需要等待一个函数或调用时,我们就使用await关键字。现在,引擎盖下发生的事情比这多得多,但这是理解这一点最起码需要知道的。除了asyncawait,你可能还会看到其他一些模式,包括回调和承诺。

现在我们又声明了一个变量,这是一个名为fundGame的常量。这个函数让我向合同发送一些初始以太币,这样当我们开始测试时,我们的合同就有一些资金支付给任何赢家。没有这个以太坊,任何导致获胜的测试都将失败,因为合同没有足够的资金来支付。看看这个:它也使用了await关键字,因为一旦我们调用了这个函数,并不意味着执行已经完成。我们需要等待该区块被开采,然后才能认为操作成功:

const fundGame = await gaming.fundGame({from: owner, value: web3.utils.toWei('10', 'ether')})
 })

现在,我们终于准备好编写一些 JavaScript 测试了。我们的测试以单词it开始,然后有一个描述应该发生什么的句子。在这里实际使用单词should是一种常见的习惯,这样它读起来就像一个句子;在这种情况下,它应该记录玩家的损失,所以让我们看看我们是如何做到这一点的。我们声明了一个名为gameRound的常量,我们将再次使用await来调用我们游戏契约中的winOrLose函数。请记住,这是我们的 UI 在玩家玩游戏时将要调用的同一个函数,所以我们实际上是在模拟真实的用户行为。我们的winOrLose函数有两个参数:在屏幕上显示给玩家的数字和他们对神秘数字是高还是低的猜测。我希望这个测试能够确保当玩家失败时,记录的失败次数会增加;这意味着我需要确保当winOrLose函数返回时,这是一个失败的回合。我可以提供 10 作为显示给用户的数字,true 表示他们猜测这个神秘的数字会更高。

好吧,因为我们的神秘数字是从09的一位数,它不可能高于 10,这确保了我们的测试玩家总是会输。这个函数调用的下一个重要部分是可选的第三个参数。前两个参数在我们的函数调用中定义。第三个参数来自 Solidity,它采用 JavaScript 对象的形式。在里面,我们指定了我们的from账户,意思是我希望这个交易来自的账户,也就是我们的玩家 1。我还可以给它附加一个值,代表玩家的赌注。现在以太坊网络发送和接收的所有资金都以 Wei 计价,如果你从第一天起就记得,这意味着十的十八次方Wei等于一个以太。但是 Truffle 并没有自己进行计算,而是为我们提供了一个测试时使用的 Web3 实例。

Web3 是一个 JavaScript 实用程序库,用于与以太坊网络上的智能合约进行交互,因此我们可以使用web3.utils.toWei函数将一个以太转换为 Wei,并且仍然有可读的代码。所以,我们的玩家开始了一轮游戏。由于这个await关键字,我们代码的执行将在这里等待这一轮完成,然后一旦完成,我们就可以创建一个名为player stats的新常量。这是您昨天创建的用于增加输赢次数的结构:

it('Should record player losses', async() => {
    const initialBalance = await gaming.winOrLose(10, true, {
        from: player1,
        value: web3.utils.toWei('1', 'ether')
 })

Players是地址到player结构的映射,这意味着它将以太坊地址作为参数来获取正确玩家的存储。我们可以在这里使用player1变量名,Truffle 会自动将其转换为所需的地址参数。现在我们终于可以用一个圆来验证我们期望的数字等于数字 1。如果测试失败,我们还可以在这里显示一条消息。不过,就在这里,你可能会想知道这到底是怎么回事。我们的玩家映射返回一个包含玩家输赢的结构,但是 JavaScript 不知道什么是结构——这是一个可靠性问题——所以,它从结构转换成数组,并按照变量在结构声明中列出的顺序实现。因此,我们知道当这个数组返回时,数组中的第一项将是赢,第二项将是输:

const postBalance = await gaming.players(player1)
assert.equal(playerStats[1].toNumber(), 1, 'The player should have 1 loss')
 })

numbers、Solidity 和 JavaScript 之间也有一些类型差异。当我们从 Solidity 得到一个数的时候,不管是有符号还是无符号的整数,都是一个很大的数。这实际上是一个 JavaScript 类型,我不是说这是一个很大的数字,所以我们要做的是将它转换成一个 JavaScript 数字,这样我们就可以在我们的应用程序中使用它,我们使用toNumber函数来完成。

所以,我们再做一件事。既然我们在这里,让我们验证当这个玩家输了,我们拿了他们的钱。这是经营赌博业务的一个重要部分,我想进行一些测试,以确保它正常工作。在我们玩这一轮之前,让我们得到玩家的帐户余额;我们将使用web3.eth.getBalance函数,并提供玩家的地址:

const initialBalance = await web3.eth.getBalance(player1).toNumber()

现在,在我们玩了这一轮后,我们知道玩家已经输了,我们可以用下面的代码重新获得平衡:

const postBalance = await web3.eth.getBalance(player1).toNumber()

现在,我们可以断言使用的是AtLeast函数。我用isAtLeast的原因是因为除了玩家刚输的 10 以太,他们还得为交易费交一些气。因此,初始余额应该大于最终余额加上下注金额。他们的余额应该减少了 10 英镑多一点,因为他们下注的 10 英镑加上汽油。这不是一个确切的数字,但它足够接近,我们可以确认玩家实际上输掉了我们预期的金额:

assert.isAtLeast(initialBalance, postBalance + 10, 'some message here')

我们可以在我们一直在做的同一个函数里做这个。在同一个函数中有多个断言是完全可以接受的,只要它们在你的代码中测试同一个组件或函数。现在,我们可以测试我们的功能,评估和断言结果,以及检查测试网络中不同帐户的余额。在下一部分,让我们看看如何让他们一起玩。

运行测试套件

到今天为止,我们花了很多时间写测试,没有时间运行测试。当我写代码时,我通常采取的方法是写一个单独的测试,运行测试套件以确保它失败,然后写必要的代码使它通过;这意味着我运行了很多测试,快速完成测试很重要。你已经在运行测试了:你每天都用它们来检验你的作业。当你运行松露测试,它运行测试,现在你知道这些测试来自哪里。

您还知道,为了通过这些测试,您必须运行 Ganache,因此 Truffle test 和 Ganache 之间必须进行某种通信:

好吧,让我们看看窗帘后面。Truffle 知道如何与 Ganache 交谈的原因是由于这里的这个文件—truffle.js—特别是Network部分。当您运行 Truffle 测试时,除非您另外告诉它,否则它会在我们的开发配置中假设一个开发网络:我们指定一个 localhost 地址和一个端口 7545,这是 Ganache 运行的端口。

最后,我们告诉它使用 Ganache 提供的任何网络 ID,这将与其他配置有所不同,您将在第 7 章第七天 - 学习部署到网络。如果您更改了 Ganache 中的任何设置,或者决定使用其他本地以太坊客户端,您需要在这里更新这些设置,以确保 Truffle 知道如何与它对话。至于运行测试套件,这就是全部内容。键入以下 Truffle 测试:

module.exports = {
    migrations_directory: "./migrations",
    solc: {
    optimizer: {
        enabled: true,
        runs: 2000
        }
 },
 networks: {
     development: {
         host: "127.0.0.1",
         port: 7545,
         network_id: "*" // Match any network id
     },
     ropsten: {
         host: "127.0.0.1",
         port: 8545,
         network_id: 3,
         from: "0xe7d6c3f43d7859d7d6d045f9ac460eedffd3eae6"
     }
  }
};

但是既然我们在这里,让我问你这个问题:如果测试失败了你会怎么做?在您考虑如何处理这个问题的同时,让我向您展示一下我最喜欢的使用 Truffle 调试器的特性之一。

让我们快速浏览一下我们的winOrLose函数。我将在这里添加一个新的require语句,require(1 != 1),出于正常目的,是的,这将是一件愚蠢的事情,但它将确保我们的函数调用失败,允许我向您展示如何调试它:

function winOrLose(unit display, bool guess) extrenal payable returns (bool, unit) {
     /* Use true for a higher guess, false for a lower guess*/
     require(online == true, "The game is not online");
     require(msg.sender.balance > msg.value, "Insufficient funds");
     require(1 != 1);
     unit mysteryNumber_ = mysteryNumber_, display, guess);
     if (isWinner == true) {
          /* Player won */
          msg.sender.transfer(msg.value * 2);
          return (true, mysteryNumber_);
     } else if (isWinner == false) {
          /* Player lost */
          return (false, mysteryNumber_);
     }
}

我将切换到终端会话并启动一个truffle develop控制台。当你这样做时,Truffle 启动了,它带来了自己的以太坊网络,无论何时启动,你都可以在这里看到。我们得到了一些帐户私钥和助记符,如果我们想将钱包连接到它,我们可以使用它们:

我还将打开第二个控制台窗口,并在其中运行truffle develop --log命令。在我们的开发者控制台中,我们将compile我们的合同,然后我们将键入migrate --reset,这将它迁移到这个本地以太坊网络:

现在,我将执行这个命令,它调用我们契约上的一个函数;但是这看起来就像一团乱麻,所以让我们一点一点地分解它,这样我们就能理解这里发生了什么。这是同一件事,它只是在控制台中写成一行,但在这里我们将把它分成多行来演示每一部分是什么。我们已经得到了我们的游戏合同,也就是以太坊合同,我们在上面调用了deployed方法,就像我们在测试中所做的一样。然后,我们有一个承诺,所以当这个承诺实现时,我们调用 doc,或者我们有一个函数接收该契约作为实例的变量名,在该函数内部,我们将返回instance.winOrLose函数,该函数在我们的智能契约中执行 winOrLose 函数。完成后,我们将有另一个点,或者我们调用另一个函数来写出以太坊网络对我们控制台的响应:

Gaming.deployed()
.then(function(instance) {
   return instance.winOrLose(5, true);
})
.then(function(value) {
   console.log(value)
});

现在我们可以执行了。每当我们执行它时,我们都会得到 VM 异常处理事务恢复,所以它失败了:

不过,我们希望看到的是,我们获得我们的事务 ID,这也是我在这里使用开发控制台的原因。因此,现在我可以获取该事务 ID,键入debug,并粘贴该事务 ID,它将引导我完成该事务的所有操作。你现在看到的是,它会一次遍历一行代码。我们可以在这里输入一些命令,我们将一次遍历一行,为了跳过,我们还可以设置断点并观察表达式。它实际上是一个非常全功能的调试器:

它在这里给我看,带下划线的,将要执行的代码行;所以当我们准备好执行它时,我们可以点击回车,然后它继续执行我们的winOrLose函数。现在它将评估函数中的每个参数。我们已经进入了函数中的第一行代码,require 语句。它正在评估变量online,这是true并将变量评估为true,现在它将评估整个语句,现在它将评估require语句,我们将做同样的事情来确保msg.sender.balance大于消息值。这里很冗长,对吧:

现在我们重定向到1!=1,所以它只是把我们踢出去,说它因运行时错误而停止。真正酷的是,我们现在确切地知道了合同中的哪一行代码导致了执行失败。

现在你知道如何用 Solidity 和 JavaScript 编写你的测试了。您知道如何运行它们,并且学会了在出错时如何使用 Truffle 交互式调试器。让我们开始今天的作业吧。

分配

在过去的几天里,您一直在编写代码,并依靠测试来让您知道代码何时是正确的。今天,我们要翻转那些桌子。契约中有一个函数叫做withdrawFunds。它不带任何参数,并将合同的余额转移给消息发送者。我希望您编写一个测试,获取我们测试中定义的所有者的合同余额,调用withdrawFunds函数,然后验证所有者余额增加了 10 ether。

作为一个额外的赋值,你也可以写一个额外的断言来确保合同余额在撤销后为零。现在,您将希望用 JavaScript 来做这些测试,因为您不能使用可靠性测试来访问以太坊帐户。如果遇到困难,可以看看我们的 JavaScript 测试文件中已经编写的一些现有测试。使用这些作为灵感实际上没有错:阅读他人编写的代码是增加您自己对特定主题的理解的好方法。

摘要

我们已经到了这一章的结尾了!我们看到了单元测试和集成之间的比较。然后,我们寻找编写测试背后的原因。之后,我们测试了各种应用的策略,例如,Solidity 和 JavaScript。接下来,我们创建了一个单元测试,并学习了可靠性测试惯例。我们学习了如何为同一个功能创建多个测试,还用 JavaScript 创建了集成测试。最后,我们运行了测试套件。

在下一章,我们将为我们的应用程序建立一个用户界面。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组