调试 Tact 合约
作为智能合约开发人员,我们编写的代码并不总是能实现我们的预期。 有时,它做的事情完全不同! 当意外发生时,接下来的任务就是找出原因。 为此,有多种方法可以揭示代码中的问题或 “错误”。 让我们开始_调试_!
一般方法
目前,Tact 还没有步进式调试器。 尽管如此,仍然可以使用”printf 调试” 方法。
这包括在整个代码中主动调用 [dump()
][dump]和 dumpStack()
函数,并观察特定时间点的变量状态。 请注意,这些函数只在 [调试模式](#debug-mode)下工作,否则不会执行。
除了转储值之外,使用 require()
、nativeThrowIf()
和 nativeThrowUnless()
等自信的函数通常也很有帮助。 它们有助于明确说明你的假设,并方便设置 “绊线”,以便在将来发现问题。
如果您没有找到或无法解决您的问题,请尝试在 Tact 的Telegram 聊天中询问社区;如果您的问题或疑问与 TON 的关系大于与 Tact 的关系,请进入TON Dev Telegram 聊天。
常用调试功能
Tact 提供了大量对调试有用的各种函数:核心库 → 调试。
在编译选项中启用调试模式
Using @tact-lang/emulator
To make dump
work you need to enable the feature debug
in tact.conf.json
.
如果您正在处理基于 Blueprint 的项目,可以在合约的编译配置中启用调试模式,这些配置位于名为 wrappers/
的目录中:
请注意,从 0.20.0 开始的 Blueprint 版本会自动为新合约启用 wrappers/
中的调试模式。
除此之外,蓝图 项目中仍可使用 tact.config.json
。 在这种情况下,除非在 wrappers/
中修改,否则 tact.config.json
中指定的值将作为默认值。
在蓝图中编写测试,使用 Sandbox 和 Jest
蓝图 是一个流行的开发框架,用于在 TON 区块链上编写、测试和部署智能合约。
Ton Emulator allows you to have a small virtual blockchain in your Node.js code. This library is built specifically for testing smart contracts in unit tests.
无论何时创建一个新的 Blueprint 项目,或在现有项目中使用 “blueprint create “命令,都会创建一个新的合同以及测试套件文件。
这些文件被放在tests/
文件夹中,并用[Jest][jest]执行。 默认情况下,除非指定特定组或测试关闭,否则所有测试都会运行。 有关其他选项,请参阅 Jest CLI 中的简要文档:jest --help
。
测试文件的结构
假设我们有一份名为 Playground
的合同,写在 contracts/playground.tact
文件中。 如果我们通过 Blueprint 创建了该合约,那么它也会为我们创建一个 tests/Playground.spec.ts
测试套件文件。
测试文件包含一个 describe()
[Jest][jest] 函数调用,表示一个测试组。
在该组中,有三个变量在所有测试中都可用:
blockchain
- 由[沙盒][sb]提供的本地区块链实例- deployer` - 一个 TypeScript 封装器,用于部署我们的 Playground 合约或我们希望部署的任何其他合约
playground
- 我们的Playground
合约的 TypeScript 封装器
然后,调用一个 beforeEach()
[Jest][jest] 函数—它指定了在每个后续测试闭包之前要执行的所有代码。
最后,通过调用 it()
[Jest][jest] 函数来描述每个测试闭包—这就是实际编写测试的地方。
Example of a minimal test file:
使用 dump()
调试
要查看 [dump()
][dump]函数调用的结果,并使用”printf 调试” 方法,就必须
- 在代码的相关位置调用 [
dump()
][dump]和其他常用调试函数。 - 运行 [Jest][jest]测试,这些测试将调用目标函数并向目标接收器发送信息。
假设你已经创建了一个 新计数器合约项目,让我们来看看它是如何实际运行的。
首先,让我们在 contracts/simple_counter.tact
中调用 [dump()
][dump],这将把 msg
[Struct][struct] 中传递的 amount
输出到合约的调试控制台:
接下来,让我们注释掉 tests/SimpleCounter.spec.ts
文件中所有现有的 it()
测试闭包。 然后再加上下面一条:
它向我们合约的 receive(msg: Add)
接收器 发送信息,而不存储发送结果。
现在,如果我们使用 yarn build
构建我们的合约,并使用 yarn test
运行我们的测试套件,我们将在测试日志中看到以下内容:
这是由我们上面的 [dump()
][dump]调用产生的。
使用expect()
说明期望
编写测试不可或缺的部分是确保你的期望与观察到的现实相吻合。 为此,[Jest][jest] 提供了一个函数 expect()
,使用方法如下:
- 首先,提供一个观测变量。
- 然后,调用特定的方法来检查该变量的某个属性。
下面是一个更复杂的示例,它使用 expect()
函数来检查计数器合约是否确实正确地增加了计数器:
实用方法
由 Blueprint 生成的测试文件导入了 @ton/test-utils
库,该库为 expect()
[Jest][jest] 函数的结果类型提供了一些额外的辅助方法。 请注意,toEqual()
等常规方法仍然存在,随时可以使用。
有交易
方法 expect(…).toHaveTransaction()
检查事务列表中是否有符合你指定的某些属性的事务:
要了解此类属性的完整列表,请查看编辑器或集成开发环境提供的自动完成选项。
toEqualCell
方法 expect(…).toEqualCell()
检查两个 单元格是否相等:
对等切片
方法 expect(…).toEqualSlice()
检查两个 slices 是否相等:
toEqualAddress
方法 expect(…).toEqualAddress()
检查两个 地址是否相等:
发送信息至
要向合约发送消息,请在其 TypeScript 封装器上使用 .send()
方法,如下所示:
消息体可以是简单的字符串,也可以是指定 消息类型字段的对象:
通常情况下,存储此类发送的结果非常重要,因为它们包含发生的事件、进行的事务和发送的外部信息:
这样,我们就可以轻松地过滤或检查某些交易:
遵守收费和价值
[沙盒][sb]提供了一个辅助函数 printTransactionFees()
,它可以漂亮地打印所提供交易的所有值和费用。 它对观察 [纳米通币](/book/integers#nanotoncoin)的流动非常方便。
要使用它,请在测试文件顶部修改来自 @ton/sandbox
的导入:
然后,提供一个事务数组作为参数,就像这样:
要处理计算和操作 阶段的总费用或费用的单个值,请逐个检查每笔交易:
有故意错误的交易
有时,进行负面测试也很有用,它可以故意出错并抛出特定的退出代码。
蓝图中此类[Jest][jest]测试闭包的示例:
请注意,要跟踪具有特定退出代码的事务,只需在 expect()
方法的 toHaveTransaction()
对象参数中指定 exitCode
字段即可。
不过,通过指定收件人地址 “to “来缩小范围是很有用的,这样 Jest 就只能查看我们发送给合同的消息所引起的事务。
模拟时间流逝
由 Sandbox 提供的本地区块链实例中的 Unix 时间从 beforeEach()
块中创建这些实例的时刻开始。
在此之前,我们曾被警告不要修改 beforeEach()
块,除非我们真的需要这样做。 而现在,我们要做的,就是稍稍推翻时间和时空旅行。
让我们在末尾添加下面一行,将 blockchain.now
明确设置为处理部署消息的时间:
现在,我们可以在测试子句中操作时间了。 例如,让我们在部署一分钟后进行一次交易,两分钟后再进行一次交易:
通过 emit
记录
全局静态函数 emit()
向外部世界发送信息—它没有特定的接收者。
该功能对于记录和分析链外数据非常方便,只需查看合约生成的 external messages 即可。
本地沙箱测试中的日志
在 [Sandbox][sb] 中部署时,您可以从 receiver function 中调用 emit()
,然后观察已发送的 external messages 列表:
已部署合同的日志
TON 区块链上的每笔交易都包含out_msgs
- 这是一个字典,保存着执行交易时创建的传出消息列表。
要查看字典中 emit()
的日志,请查找没有收件人的外部消息。 在各种 TON 区块链探索器中,此类交易将被标记为 “外部输出”,目的地指定为”-“或 “空”。
请注意,有些探索者会为你反序列化发送的信息体,而有些则不会。 不过,您可以随时在本地[自行解析](#logging-parsing)。
解析已发送信息的正文
Example:
现在,让我们为 “Bonanza “合同制作一个简单的 [测试条款](#tests-structure):
在这里,res
对象的externals
字段将包含已发送的[外部信息](/book/external)列表。 让我们访问它,以解析通过调用 Tact 代码中的 emit()
(或简称 emitted)发送的第一条信息:
要解析第二条发出的信息,我们可以手动使用一堆 .loadSomething()
函数,但这样做太麻烦了—如果 Ballroom
[Struct][struct] 的字段发生变化,就需要重新开始。 当你以这种方式编写大量测试时,可能会适得其反。
幸运的是,Tact 编译器会自动为合约生成 TypeScript 绑定(或封装),在测试套件中重新使用它们非常容易。 它们不仅有一个你正在测试的合约的包装器,而且还导出了一堆辅助函数来存储或加载合约中定义的 [Structs][struct] 和 [Messages][message] 。 后者的命名方式与 [Structs][struct] 和 [Messages][message] 一样,只是在前面加上了 load
前缀。
例如,在我们的例子中,我们需要一个名为 loadBallroom()
的函数,用于将 [Slice
][slice]解析为 TypeScript 中的 Ballroom
[Struct][struct] 。 要导入它,要么键入名称,让集成开发环境建议自动导入,要么查看测试套件文件的顶部—应该有类似的一行:
现在,让我们来解析第二条发出的信息:
请注意,即使在我们的测试套件之外,也可以解析已部署合约的发射信息。您只需获取发射的消息体,然后像上面的示例一样,在 @ton/core
库旁使用自动生成的 Tact 的 TypeScript 绑定。
处理退回的邮件
当 send 带有 bounce: true
时,信息会在出错时反弹。确保编写相关的 bounced()
消息接收器,并优雅地处理被退回的消息:
请记住,在 TON 中被退回的邮件正文中只有 个可用数据位,而且没有任何引用,因此无法从中恢复很多数据。 不过,您仍然可以看到邮件是否被退回,从而可以创建更稳健的合同。
了解更多有关退信和收信人的信息:退信。
实验实验室设置
如果你对 Blueprint 的测试设置感到不知所措,或者只是想快速测试一些东西,不用担心—有一种方法可以建立一个简单的游戏场作为实验实验室,来测试你的想法和假设。
-
创建新的蓝图项目
这将防止任意代码和测试污染您现有的程序。
新项目可以取任何名字,但我会取名为 “Playground”,以表达正确的意图。
要创建它,请运行以下命令:
从 0.20.0 开始的 Blueprint 版本会自动为新合约启用
wrappers/
中的调试模式,因此我们只需调整测试套件并准备好我们的Playground
合约即可进行测试。 -
更新测试套件
移动到新创建的
tact-playground/
项目,在tests/Playground.spec.ts
中,将"should deploy"
测试闭包改为以下内容: -
修改合同
用以下代码替换
contracts/playground.tact
中的代码:此设置的基本思想是将要测试的代码放入 receiver function 中,以响应 string 消息
"plays"
。请注意,您仍然可以在[接收器](/book/contracts#receiver-functions)之外编写任何有效的 Tact 代码。 但为了测试它,你需要在其中编写相关的测试逻辑。
-
我们来测试一下!
这样,我们的实验装置就完成了。 要执行我们为 “Playground “合约准备的单个测试,请运行以下程序:
从现在起,您只需修改 Tact 合同文件中已测试的 [receiver function](/book/contracts#receiver-functions)的内容,然后重新运行上述命令,就可以对某些内容进行测试。 冲洗并重复这个过程,直到你测试了你想测试的东西。
为了简化和更干净的输出,您可以在
package.json
中为scripts
添加一个新字段,这样您只需运行yarn lab
即可在一个字段中完成构建和测试。在 Linux 或 macOS 上,它看起来就像这样:
下面是它在 Windows 上的样子:
要运行