热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Cairo之旅IX:用Protostar编写测试合约

作者:DaringtonNnam原文:JourneyThroughCairoIX—UltimateGuideToTestingYourContrac

作者:Darington Nnam 原文:Journey Through Cairo IX— Ultimate Guide To Testing Your Contracts With Protostar 翻译:Louis Wang 校对:「StarkNet 中文社区」

欢迎来到我们的系列文章「Cairo 之旅」第九讲!上一讲我们开始部署 Starknet 合约,今天我们开始测试合约。

像往常一样,如果你是中途加入,建议从头开始看我们的文章。

单元测试

单元测试不仅作为软件工程中广泛使用的术语,同样适用于智能合约开发中。因此在学习前,先通过几句话了解什么是单元测试。

单元测试是一种对软件的单个单元或组件进行测试。单元测试一般在软件应用的开发阶段进行,确保某个应用所有部分都按预期运行。它们通常用于软件开发的各个领域,但在编写智能合约时有更重要的作用。

当编写大量代码时,很有可能会存在现有功能错误,或者与预期执行不相符。经常会出现智能合约通过了编译但仍然存在代码错误的情况。

虽然大多数开发人员都不爱写测试,或者写覆盖面小的测试,但是制作测试有利于:

  1. 单元测试有助于在应用开发早期修复错误,避免日后被攻击造成亏损。

  2. 有助于开发人员理解测试代码库,以便做出修改。

  3. 高质量的单元测试可以作为项目(指南)文档。

明白了写测试的重要性后,让我们深入了解一下如何为 Cairo 合约写测试吧!

Protostar 测试

类似于 Foundry 让 Solidity 开发者在 Solidity 中编写单元测试,感谢 Protostar 团队的努力让 Cairo 开发者在 Cairo 中编写单元测试更容易!

基本语法

Protostar 的测试实例:

@externalfunc test_increase_balance{syscall_ptr: felt*, range_check_ptr, pedersen_ptr: HashBuiltin*}() { let (result_before) = balance.read(); assert result_before = 0; increase_balance(42); let (result_after) = balance.read(); assert result_after = 42; return ();}

如上述,有了 Protostar 就可以用 Cairo 写测试。从这段代码中,你可以发现关于编写单元测试:

  1. 所有的测试用例都是外部函数,并以 test_ 为前缀。

  2. 在这里没有给函数传递参数,因为我们手动提供了所有需要的测试参数。

  3. 可以使用 assert 关键字更容易进行比较。

注意:在 Cairo 中使用 assert 关键字,如果左边的变量还没有设置,就会自动把右边的变量分配给左边的变量,因此安全的做法是确保我们要比较的常数总是在左边。

为了进一步解释这个问题,假设我们有一个常数。

const NUMBER = 30;

我们想获得一个函数的返回值并检查它是否等于常数,首先确保常数在左边,如果函数返回一个空参数,我们不想让 Cairo 分配常数。

所以我们需要改写:

let (num) = get_number();assert NUMBER = num;

设置钩子

在测试用例之前需要进行某些操作,比如部署一个合约并记录其地址,设置一些重要变量等。

类似于在 mochachai 中使用的 before 钩子 (Hook),我们可以在 protostar 中使用 setup 钩子预先在名叫 context 的存储变量中设置一些变量,并将它们从一个函数传递到另一个函数。

例如,我们可以使用设置钩子来部署我们在上一篇文章中的 starknet 合约,并将合约地址存储在上下文中,然后传递给其他测试案例:

@externalfunc __setup__{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {%{context.address = deploy_contract("./src/starknet.cairo", [ids.NAME]).contract_address %}return ();}

在开始写测试时会进一步说明。

常见的作弊代码

引用 protostar 官方文档中的话「大多数时候,不能只用断言来测试智能合约。一些测试案例需要操作区块链的状态,以及检查还原和事件。为此,Protostar 提供了一套作弊代码。」

还需要注意的是,这些作弊代码只能通过提示来访问,而不应该明确地写在你的 Cairo 合约中!

你可以在这里找到全部的,但为了控制篇幅,我们只介绍今天用到的四个:

  1. deploy_contract

  2. expect_revert

  3. expect_events

  4. start_prank

deploy_contract

这个作弊代码部署一个合同,输入合同的相对路径和构造函数参数(如果有的话)。

要使用这个作弊代码,我们要传入合同代码的相对路径,以及构造函数的参数:

%{ deploy_contract("./src/starknet.cairo", [322918500091226412576622]) %}

由于部署合约的过程通常很慢,建议你在设置钩子中使用这个作弊代码,这样你只需要执行一次这个动作。deploy_contract 作弊代码还可以访问已部署合同的合同地址,可以访问并存储在一个上下文变量中,以便从测试案例中访问。

%{context.address = deploy_contract("./src/starknet.cairo", [[ids.NAME](http://ids.name/)]).contract_address %}

expect_reverts

这个作弊代码是用来检查它下面的某个操作是否以指定的错误恢复,如果没有,则测试失败。换句话说,你可以用这个测试来确认合约回滚情况是否按预期工作。

例如,如果我们通过 main.cairo(由 protostar 初始化时创建的默认合约)的测试,我们会发现下面这段代码,它测试函数 increase_balance 会在输入为负数时回滚。

%{ expect_revert("TRANSACTION_FAILED", "Amount must be positive") %}increase_balance(-42);

可以看到,expect_revert 执行了它下面的函数调用,并检查错误的类型是否为 “TRANSACTION_FAILED”,以及是否符合 “Amount must be positive”,如果不符合则测试失败。

expect_events

这个作弊代码帮助你检查从你的 Starknet 合约中发出的事件是否与一些预期的事件相匹配。

expect_revert 不同,你可以在函数测试案例中的任何地方使用这个作弊代码,因为 Protostar 在测试案例完成后会检查发出的事件:

%{ expect_events({"name": "stored_name", "data" : [ids.CALLER, [ids.NAME](http://ids.name/)]}) %}

start_prank

这个作弊代码在编写单元测试时是非常重要的。你可以用它在编写单元测试时将 caller_address 改为选定的任何地址。使用这个代码比相对麻烦,因为使用时必须初始化一个持有新地址的可调用程序(像一个状态),然后在完成后取消初始化它。

也可以初始化不止一个来进行不同地址的测试:

%{ stop_prank = start_prank(0x00A596deDe49d268d6aD089B5aBdD089BE92D089B191e48) %} // Your test logic goes here.%{ stop_prank() %}

我们使用 start_prank 开始一个 prank,并同时初始化一个可调用的 stop_prank。我们可以通过调用 stop_prank() 来结束 prank,在 start_prankstop_prank() 之间的任何函数调用将使用指定地址作为调用者地址。

编写我们的第一个测试

哇,我们已经讲了很多了。现在是时候实践知识了,为我们上一篇文章中的 Starknet 合约写一个测试。

你也可以查看合约代码。

测试分为五个部分检测我们到目前为止所学的所有知识。

  1. 指明必要的导入。

  2. 指明整个测试所需的一些常量。

  3. 使用钩子部署我们的合约。

  4. 测试 store_name 函数。

  5. 测试 get_name 函数。

指明必要的导入

对于这个测试,我们将导入 HashBuiltin 库函数,以及我们想在 Starknet 合约中运行测试的所有函数(store_nameget_name 函数)。

%lang starknetfrom starkware.cairo.common.cairo_builtins import HashBuiltinfrom src.starknet import store_name, get_name

指明整个测试所需的一些常量

在这个测试中,我们需要两个常量:我们打算用来开始测试的呼叫地址,以及我们想作为参数提供给 store_name 函数的名称(用 felts 表示)。

const CALLER = 0x00A596deDe49d268d6aD089B56CC76598af3E949183a8ed10aBdE924de191e48;const NAME = 322918500091226412576622;

使用钩子部署我们的合约

如何用钩子部署合约:

@externalfunc __setup__{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {%{context.address = deploy_contract("./src/starknet.cairo", [ids.NAME]).contract_address %}return ();}

从上面的代码中,首先通过使用函数名 setup 来指定我们正在使用一个设置钩子。然后使用 deploy_contract 作弊代码来部署我们的合约,提供我们的合约代码的路径,以及一个参数 NAME

注意我们使用 ids.NAME,而不是仅仅使用 NAME,这就是我们在 hint 中访问 Cairo 常量的方法。

测试 store_name 函数

@externalfunc test_store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { %{ stop_prank = start_prank(ids.CALLER) %} store_name(NAME); %{ expect_events({"name": "stored_name", "data" : [ids.CALLER, ids.NAME]}) %} %{ stop_prank() %} return ();}

测试可以帮助你理解一个函数的行为方式,从我们的函数中,你会注意到我们得到了 caller_address,然后我们用它作为一个键来存储我们的 name 参数。

在 Protostar 中,caller_address 默认为 0,但可以使用 start_prank 来改变这个。因此,你可以从上述代码中看到,首先需要启动一个 prank 来改变来呼叫地址。

接下来我们调用 store_name 函数,提供前面的常量 NAME 作为参数。

最后,我们检查 Starknet 的状态中发出的事件,以确保它与我们提供的参数 (CALLER 和 NAME) 相匹配,最后才停止 prank。

测试 get_name 函数

@externalfunc test_get_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { %{ stop_prank = start_prank(ids.CALLER) %} store_name(NAME); let (name) = get_name(CALLER); assert NAME = name; %{ stop_prank() %} return ();}

这个测试非常简单。我们再次重复前面的过程,因为我们需要存储一个名字然后获取这个名字。

所以我们从 prank 开始,存储一个名字,然后调用 get_name 函数,提供常数 CALLER 作为参数。

需要注意这一行:

assert NAME = name;

正如你所看到的,我们遵守了前面的规则,把常数 NAME 放在左手边,这样 Cairo 就不会进行赋值而是比较。

我们的完整代码:

%lang starknetfrom starkware.cairo.common.cairo_builtins import HashBuiltinfrom src.starknet import store_name, get_nameconst CALLER = 0x00A596deDe49d268d6aD089B56CC76598af3E949183a8ed10aBdE924de191e48;const NAME = 322918500091226412576622;@externalfunc __setup__{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { %{context.address = deploy_contract("./src/starknet.cairo", [ids.NAME]).contract_address %} return ();}@externalfunc test_store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { %{ stop_prank = start_prank(ids.CALLER) %} store_name(NAME); %{ expect_events({"name": "stored_name", "data" : [ids.CALLER, ids.NAME]}) %} %{ stop_prank() %} return ();}@externalfunc test_get_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { %{ stop_prank = start_prank(ids.CALLER) %} store_name(NAME); let (name) = get_name(CALLER); assert NAME = name; %{ stop_prank() %} return ();}

最后

今天我们学习了如何用 Protostar 写测试合约,以及其他的作弊代码,它们在编写测试时可能非常有用。你也可以在这里找到 OnlyDust 的深度测试脚本,它实现了 Protostar 的大部分作弊代码。

我们将在下节课深入研究 Empiric 的预言机。如果觉得本教程对你有帮助,转发分享给其他人吧~


推荐阅读
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 基于dlib的人脸68特征点提取(眨眼张嘴检测)python版本
    文章目录引言开发环境和库流程设计张嘴和闭眼的检测引言(1)利用Dlib官方训练好的模型“shape_predictor_68_face_landmarks.dat”进行68个点标定 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了JavaScript进化到TypeScript的历史和背景,解释了TypeScript相对于JavaScript的优势和特点。作者分享了自己对TypeScript的观察和认识,并提到了在项目开发中使用TypeScript的好处。最后,作者表示对TypeScript进行尝试和探索的态度。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
  • 如何实现JDK版本的切换功能,解决开发环境冲突问题
    本文介绍了在开发过程中遇到JDK版本冲突的情况,以及如何通过修改环境变量实现JDK版本的切换功能,解决开发环境冲突的问题。通过合理的切换环境,可以更好地进行项目开发。同时,提醒读者注意不仅限于1.7和1.8版本的转换,还要适应不同项目和个人开发习惯的需求。 ... [详细]
  • 恶意软件分析的最佳编程语言及其应用
    本文介绍了学习恶意软件分析和逆向工程领域时最适合的编程语言,并重点讨论了Python的优点。Python是一种解释型、多用途的语言,具有可读性高、可快速开发、易于学习的特点。作者分享了在本地恶意软件分析中使用Python的经验,包括快速复制恶意软件组件以更好地理解其工作。此外,作者还提到了Python的跨平台优势,使得在不同操作系统上运行代码变得更加方便。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • Android源码中的Builder模式及其作用
    本文主要解释了什么是Builder模式以及其作用,并结合Android源码来分析Builder模式的实现。Builder模式是将产品的设计、表示和构建进行分离,通过引入建造者角色,简化了构建复杂产品的流程,并且使得产品的构建可以灵活适应变化。使用Builder模式可以解决开发者需要关注产品表示和构建步骤的问题,并且当构建流程发生变化时,无需修改代码即可适配新的构建流程。 ... [详细]
  • 项目运行环境配置及可行性分析
    本文介绍了项目运行环境配置的要求,包括Jdk1.8、Tomcat7.0、Mysql、HBuilderX等工具的使用。同时对项目的技术可行性、操作可行性、经济可行性、时间可行性和法律可行性进行了分析。通过对数据库的设计和功能模块的设计,确保系统的完整性和安全性。在系统登录、系统功能模块、管理员功能模块等方面进行了详细的介绍和展示。最后提供了JAVA毕设帮助、指导、源码分享和调试部署的服务。 ... [详细]
  • 本文分享了一位Android开发者多年来对于Android开发所需掌握的技能的笔记,包括架构师基础、高级UI开源框架、Android Framework开发、性能优化、音视频精编源码解析、Flutter学习进阶、微信小程序开发以及百大框架源码解读等方面的知识。文章强调了技术栈和布局的重要性,鼓励开发者做好学习规划和技术布局,以提升自己的竞争力和市场价值。 ... [详细]
author-avatar
云小白
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有