# 1.1 如何为你的游戏写一个测试脚本 在这篇教程中,我们将使用一个基于`Unity`引擎开发的游戏“大鱼来了”作为范例,向大家演示如何使用我们的`AirtestIDE`快速地编写属于你的第一个游戏自动化测试脚本,它看上去并没有想象中的那么难。 在开始之前,我们强烈推荐你事先阅读过官网提供的[5分钟上手自动化测试](http://airtest.netease.com/tutorial/Tutorial.html)教程,它大致展示了从连接手机开始,到编写脚本、运行脚本、生成报告的一系列流程,然后再回头来看这篇文章中详细描述的关于游戏脚本编写的入门介绍。 **本文约4500字,仔细阅读全文可能将花费10分钟以上(但能让你少走很多弯路!)**。 ## 1. 准备工作 假设现在我们已经在电脑上[安装](http://airtest.netease.com/changelog.html)好了`AirtestIDE`,将手上一台Android手机通过USB线连上了电脑(连接方法请参考[教程](../2_device_connection/1_android_phone_connection.html)),并且打开了接下来想要测试的游戏`大鱼来了`。 万事俱备,现在我们在`AirtestIDE`上看到的画面是这样的: ![image](1_how_to_write_the_first_script_for_your_game/1_AirtestIDE_front.png) ## 2. 基于图像识别的脚本 首先,我们将为大家介绍如何使用**基于图像识别**的方式来编写脚本,我们提供了一个开源框架[Airtest](https://github.com/AirtestProject/Airtest),它可以非常直观地在当前游戏画面上通过图像识别的方式来找出我们的目标元素,并对它进行操作。 ### Airtest的touch语句 例如,我们的第一个测试用例是:点击画面上的“闯关”按钮,进入关卡选择画面,然后选择“第一关”。 这个用例可以使用`touch`这个语句来完成,在`AirtestIDE`中可以通过点击Airtest辅助窗中的`touch`按钮,然后框选按钮来生成语句: ![image](1_how_to_write_the_first_script_for_your_game/touch.gif) 对于这个用例,我们写出了这样两行代码: ```touch(```![image](1_how_to_write_the_first_script_for_your_game/button1.png)```)``` ```touch(```![image](1_how_to_write_the_first_script_for_your_game/button2.png)```)``` 运行代码,效果就是游戏中的“闯关”按钮和“第一关”按钮依次被按下,代码运行的原理如图: ![image](1_how_to_write_the_first_script_for_your_game/airtest_touch.png) #### 进阶阅读:API文档中的touch语句 `touch`语句的代码看起来简单直观,就是在当前的游戏画面中寻找符合这张图片内容的坐标,如果找到了就点击一下这个坐标,如果没有找到相符的图片将会抛出一个异常。 假如我们想要进行更多复杂的操作,可以通过查阅`Airtest`的API文档来获取更多细节,例如了解更多的参数和用法。由于`Airtest`是跨平台的,因此所有平台都有的接口统一被写在了`airtest.core.api`中了,我们可以先在`airtest.core.api`这一章节中找到我们要了解的[`touch`接口](http://airtest.readthedocs.io/zh_CN/latest/all_module/airtest.core.api.html#airtest.core.api.touch)。 在接口文档中,描述了`touch`这个语句的作用是:对设备屏幕执行点击操作,并且是跨平台的,可以用于`Android`、`Windows`和`iOS`,各平台都有的公用参数是:点击位置的坐标`v`(或一张图片,airtest将在画面上寻找到这张图片对应的位置坐标)和点击次数`times`,以及一些其他平台相关的参数。 由于我们在这个例子中使用的是Android手机来录制脚本,因此我们可以继续查阅文档中平台相关的章节,找到[`airtest.core.android.android`模块中的`touch`接口](http://airtest.readthedocs.io/zh_CN/latest/all_module/airtest.core.android.android.html#airtest.core.android.android.Android.touch),可以看到,在`android`平台上的`touch`接口接受2个参数,一个是`pos`,也就是实际点击位置的坐标,还有一个是`duration`,通过它可以控制我们点击屏幕的时长,默认是0.01秒的一下轻触,我们能将它变为一下长按。 这是`touch`语句的另外一个示例,我们把刚才传入的图片参数替换成了一个坐标并添加了一个参数`duration`,所以这行代码可以长按某个坐标位置一秒钟: ``` touch((500, 600), duration=1) ``` > 如果觉得查阅API文档有些麻烦的话,可以直接将鼠标指针移动到`AirtestIDE`的`Airtest辅助窗`里的各个快捷按钮上,能够直接看到这个接口对应的可用参数信息。当然,我们也非常推荐各位阅读[Airtest源码](https://github.com/AirtestProject/Airtest)。 #### 更多操作语句 除了`touch`之外,我们还提供了其他几种最常见的操作语句,利用它们我们能实现更加复杂的操作: - `swipe`,可以从一个位置滑动到另外一个位置 - `wait`,可以等待画面中某个图片出现 - `exists`,判断画面中是否存在某个图片 - `text`,调用输入法,输入一段文字 - ... 除了操作语句外,我们在编写测试脚本的过程中也需要验证运行结果是否正确,`Airtest`提供了专属的断言语句,方便我们进行验证: - `assert_exists`,断言图片存在于当前画面上 - `assert_not_exists`, 断言图片不存在 - ... ### 一个完整的测试用例 了解过常用语句后,接下来我们可以通过一小段代码来实现这样的一个用例: - 打开我们的待测游戏 - 选择游戏模式 - 滑动屏幕到最右方,选择想玩的关卡“第20关” - 验证是否成功进入到了“第20关”的关卡准备画面 示例代码: ![image](1_how_to_write_the_first_script_for_your_game/script.png) 在这段代码中,我们展示了绝大多数的`Airtest`语句,利用它们能够轻松地写出逻辑复杂的自动化测试脚本了。 ### 新的用例和新的挑战 `Airtest`满足了我们简单快速地编写游戏自动化测试脚本的需求,然而它的核心技术是基于图像识别,除了上一章节中提出来的成功率较低的问题之外,我们发现它在测试一些更复杂的需求时有些力不从心。 例如我们现在有一个新的用例: - 打开商城 - 升级“无敌泡泡”道具,这将会扣除一定的金币 - 验证是否成功扣除了背包中的金币 ![image](1_how_to_write_the_first_script_for_your_game/item.png) 如图所示,尽管我们能够轻而易举地通过截图的方式来识别到金币控件,然而里面的数值难以获取。同样地,我们能够写出`touch`语句去点击升级按钮(尽管需要一些小技巧才能区分出哪一个是“无敌泡泡”对应的“升级”按钮),却难以验证背包金币是否正确扣除了画面中标注的“400”金币。 好在,我们提供了另外一种方案来更好地解决这类问题,那就是基于控件搜索的[Poco](https://github.com/AirtestProject/Poco/)框架。 ## 3. 基于控件识别的Poco 先暂时抛开上面提出的用例不提,我们简单了解一下`Poco`和`Airtest`的区别在哪里。 ![image](1_how_to_write_the_first_script_for_your_game/ide_poco.png) 这是一张在`AirtestIDE`中使用Poco插件时的截图,我们利用Poco,可以准确地定位到当前游戏画面上的元素在实际UI结构树中的位置,还能够获取到这个“Go”按钮的名字、坐标等详细信息,并且我们还能通过编写一定的筛选语句来获取到这个按钮,并对它进行点击等操作。 假设我们现在想点击画面中这个“Go”按钮,使用`Airtest`的话,我们将写出这样的语句: ```touch(```![image](1_how_to_write_the_first_script_for_your_game/touch_button3.png)```)``` 在`Poco`中,写出来的语句截然不同,却简单而不失优雅: ```python poco("Go").click() ``` 这是一个简化的原理图,对比了`Airtest`和`Poco`的操作原理: ![image](1_how_to_write_the_first_script_for_your_game/poco_touch.png) 可以看到,`Poco`最大的区别就是多了一个`Poco-SDK`模块,我们需要**将`Poco-SDK`嵌入到被测游戏中**,才能够顺利地获取到UI结构树,从而进行接下来的解析和处理操作。 ### 如何接入Poco 在这一个章节里,我们将介绍如何让你的游戏接入`Poco`,享受它的强大之处。 `Poco`目前支持以下几种模式(不仅仅包含游戏引擎): - Cocos2dx-js,Cocos2dx-lua -> [接入文档](https://poco-chinese.readthedocs.io/zh_CN/latest/source/doc/integration.html#cocos2dx-lua) - Unity3D -> [接入文档](https://poco-chinese.readthedocs.io/zh_CN/latest/source/doc/integration.html#unity3d) - Android 原生APP -> 无需接入 - iOS -> [帮助文档](../2_device_connection/4_ios_connection.html) - 网易自研引擎 - 其他引擎 -> 可[自行接入](https://poco-chinese.readthedocs.io/zh_CN/latest/source/doc/implementation_guide.html) - 更多平台更多引擎,敬请期待 **简而言之,对于游戏测试来说,想要使用`Poco`就需要事先根据接入文档,将`Poco-SDK`接入到你想要测试的游戏中,如果对于文档有疑问,可以邀请你的项目组程序来帮忙一起阅读和接入。** ### 上手写Poco脚本 好了,假设在项目组程序大哥的帮助下,我们已经把测试用的`大鱼来了`游戏接入了Poco的Unity3D版本的SDK,接下来的脚本编写就很简单了: ![image](http://airtest.netease.com/tutorial/gif/poco_auto_record.gif) 基本的使用流程类似这样: - 在Poco辅助窗中**选择对应的模式**,例如这里我们选择Unity - 脚本编辑窗口将提示是否**插入对应的初始化语句**,点击yes - 空白的脚本中将被插入两行poco的初始化语句内容 - 等待几秒钟,Poco辅助窗中将显示出当前游戏画面中的**UI树结构** - 单击UI树上的节点,将显示节点内容到下方log窗口中;双击UI树上的节点,将自动插入该节点对应的代码到编辑框中;点击暂停按钮,可以将当前UI树冻结,方便查看更详细的信息 - 拿到我们需要的节点后,我们可以对它进行操作,例如`.click()`,`.swipe()` 接下来,回到我们刚才的那个使用`Airtest`难以完成的用例: - 打开商城 - 升级“无敌泡泡”道具,这将会扣除一定的金币 - 验证是否成功扣除了背包中对应数量的金币 于是我们可以写出一个这样的示例脚本: ```python # 请务必先启动待测的app,再对poco进行初始化 start_app("fish.package") # 初始化Poco,非常重要! from poco.drivers.unity3d import UnityPoco poco = UnityPoco() money = int(poco("Money").child('Text').get_text()) # .get_text()能够帮助我们获取到它的值 cost = int(poco("2024").child("Upgrade").child("NonFullPanel").child("Cost").child("Number").get_text()) # 点击待测道具的升级按钮,请注意末尾的.click() poco("2024").child("Upgrade").child("NonFullPanel").child("UpgradeBtn").click() current_money = int(poco("Money").child('Text').get_text()) assert_equal(money-cost, current_money, "成功扣除对应金币") ``` 对于新手来说,有几个点非常容易出错: - **poco的初始化应该放在游戏启动以后再进行**,因为poco需要与游戏中的poco-sdk进行通信,需要等待游戏将poco-sdk初始化完成,才能初始化poco - 请务必选择**正确的Poco模式**,先弄清楚自己的游戏是什么引擎做的,然后再进行对应的SDK接入,接入成功后才能够开始使用 - poco中的UI对象只是一个**代理**,需要对它进行进一步操作(例如`.click()`,或是`.exists()`)才能达到符合我们预期的效果,举个例子: ```python # 例如,这样来判断画面中存在Go按钮的if语句写法是错误的 # 因为Poco("Go")是一个UIObjectProxy对象 if Poco("Go"): Poco("Go").click() # 正确写法应该是 if Poco("Go").exists(): Poco("Go").click() ``` ### 进阶:如何改进和完善脚本 刚才我们写出来的代码是直接使用`AirtestIDE`的Poco插件来自动生成对应的节点选择代码的(通过双击UI树上的节点),这对于新手来说非常容易操作(我们还提供了一个自动录制的功能,可以直接把当前鼠标的操作快速录制成语句),然而需要注意的是,有些时候自动生成的代码可能不符合我们的需求。 举个例子,在大鱼来了的道具选择画面中,有多个标注价格的节点,如图: ![image](1_how_to_write_the_first_script_for_your_game/poco_coin.png) 假如直接使用`AirtestIDE`选中节点,双击后生成的节点选择代码有可能是这样的: ```python # 磁力泡泡道具需要花费的金币标签 poco(text="600") ``` 乍看之下,这行自动生成的代码简洁明了,也能完成我们的需求“获取磁力泡泡道具对应的金币值”,然而这段代码直接使用了`text="600"`作为选择条件,假如这个节点的数值发生了变化、或是有其他的道具也需要花费600金币时,这行代码的运行结果可能就不符合我们的预期,因为它不能精确地定位到我们指定的特定节点。 如何修改这行代码,就需要我们对`Poco`的选择器有一定了解,通过自己编写一定的选择条件来修改它。 所以在这里我们建议大家: - 阅读我们提供的[入门教学用例](https://poco-chinese.readthedocs.io/zh_CN/latest/source/README.html#tutorials-and-examples),教学demo里提供的游戏和应用都已经接入了`Poco-SDK`,能够最快速度上手体验`Poco` - 仔细阅读`Poco`文档,学习如何[使用Poco选择UI对象](https://poco-chinese.readthedocs.io/zh_CN/latest/source/README.html#working-with-poco-objects),对于选择器的熟练掌握能够让我们写出更符合我们需求的UI选择代码 - 文档中的[操作UI对象](https://poco-chinese.readthedocs.io/zh_CN/latest/source/README.html#object-proxy-related-operation)描述了最常用的一些操作,可以先学习上手 - 当已经大致入门后,可以更深入地阅读[Poco instance API](https://poco-chinese.readthedocs.io/zh_CN/latest/source/poco.pocofw.html)和[Poco proxy object API](https://poco-chinese.readthedocs.io/zh_CN/latest/source/poco.proxy.html),以及API文档的其他章节 ## 4. 小结 这篇文章介绍了使用`Airtest`和`Poco`编写游戏测试脚本的一些入门指南,和一些新手常见问题的解答,更多进阶的内容请查阅官方文档手册。 值得注意的是,**Poco和Airtest都是作为Python第三方库引用的,本质上我们编写出的脚本都是普通的Python脚本,因此完全可以同时使用,搭配起来能够更精准地实现我们的测试需求,同时还能引入Python的其他第三方库,写出更复杂强大的脚本**。