找回密码
 立即注册
查看: 417|回复: 0

[笔记] Unity自动化测试--AltUnity

[复制链接]
发表于 2021-12-9 15:15 | 显示全部楼层 |阅读模式
在介绍AltUnity自动化测试框架之前,先了解下关于测试的一些基本概念。
测试目的

  • 以最少的人力、物力和时间找出软件中潜在的各种错误和缺陷,通过修正各种错误和缺陷保障软件质量,避免软件发布后由于潜在的软件错误和缺陷造成的隐患所带来的商业风险。
  • 同时利用测试过程中得到的测试结果和测试信息,作为后续项目开发和测试过程改进的重要输入,避免在将来的项目开发和测试中重复同样的错误;
  • 采用更加高效的测试管理手段,提高软件测试的效率和软件产品的质量。
根据项目开展的不同阶段,可划分为如下几种类型的测试

  • 单元测试(Unit tests),是所有测试中最底层的一类测试,是整个软件测试过程的基础和前提,其目的是验证每个单元是否符合预期的结果。对于"单元"的定义取决于自己。如果你正在使用函数式编程,一个单元就是一个函数。你的单元测试将使用不同的参数调用这个函数,并断言它返回了期待的结果;在面向对象语言里,下至一个方法,上至一个类都可以是一个单元。
  • 集成测试(Integration tests),在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。 实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。
  • 功能测试(Funtional tests),是一种软件测试,根据功能要求/规范来验证软件系统。功能测试的目的是测试软件应用的每个功能,通过提供适当的输入,根据功能要求验证输出。大致可分为以下三类:
  • 人机交互测试(HMI tests),验证软件的视觉表现(视觉元素位置、颜色、大小等)和操作响应(鼠标操作、触屏操作等)是否符合预期。
  • 安全测试(Security tests),用于验证系统安全性。
  • 负载测试(Load tests),用于检查系统的性能和稳健性。


手动测试与自动化测试

手动测试需要人工干预才能执行测试。自动化测试是使用工具执行测试用例
获得快速准确的视觉反馈。可以快速,准确地提供测试结果
人的判断力和直觉有益于交互操作。可以低时间成本的进行最小粒度的单元测试
任何类型的应用程序都可以手动测试,某些测试类型(例如临时测试和探索测试)更适合手动执行。可以记录自动化过程。这使您可以重用和执行相同类型的测试操作

基于Unity如何选择最适合自动化测试工具?

Unity Test Runner (UTR),一个直接集成在Unity中,使用c#和nunit library编写测试用例的测试工具;测试在Unity编辑器的 "编辑模式 "下执行,在不启动播放器的情况下进行代码检查;在 "播放模式 "下,可以直接在unity场景中看到测试效果。UTR主要用于单元测试,也可能用于集成测试。因此需要在项目的构建过程中增加更多的组件,因此增加了代码量和开发时间。并且UTR的测试需要在独立的测试环境下执行。而自动功能测试必须尽可能接近生产环境,这就是为什么UTR不是最合适的工具。
AltUnityTester,AltUnity tests拥有识别Unity场景中的对象并与之建立交互的能力。AltUnity Tester是由Altom公司开发的开源测试自动化工具,它采取了用户界面的形式,应该精确地帮助检测游戏中的对象,并使用C#、Python或Java编写的测试与它们互动。这些测试可以在真实设备(手机、PC等)或Unity编辑器中运行。
GAutomator,是一个基于Python针对手游的UI自动化测试框架。设计理念与使用方式,类似于Android的UIAutomator。GAutomator以引擎中的元素为操作对象(如Unity中的GameObject),通过操作GameObject实现UI自动化测试。基于GameObject的方式,不存在手机分辨率适配的问题,一份脚本能够运行在不同手机之上。基于GameObject的另外一个优点为鲁棒性较强,游戏的UI界面经常发生变化,GameObject变化频率相对较低。
AirtestProject,是由网易游戏推出的一款基于Python自动化测试框架,项目构成如下:

  • Airtest:是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS
  • Poco:是一款基于UI控件识别的自动化测试框架,目前支持Unity3D/cocos2dx-*/Android原生app/iOS原生app/微信小程序,也可以在其他引擎中自行接入poco-sdk来使用
AltUnity Tester

概述
主要特点

  • 找到元素并获得它们的所有(公共)属性:坐标、文本、值、Unity组件等
  • 使用和修改Unity元素得任何(公共)方法和属性
  • 模拟任何类型的设备输入
  • 操作和生成测试数据
  • 从你的Unity项目中获取屏幕截图
  • 使用你最喜欢的IDE运行C#、Python或Java测试,并在设备上或Unity编辑器内运行游戏
  • 与Appium测试集成,能够与本地元素互动
  • 在测试执行过程中实现输入动作的可视化
  • 在Unity编辑器中查看测试结果和报告
AltUnity Tester 框架包含以下几个模块:

  • AltUnity Server
  • AltUnity Driver
  • AltUnity Tester Editor Window
AltUnity Server模块用于检测你的项目,以提供对Unity层次结构中所有对象的访问能力。AltUnity Server在运行Unity应用程序的设备上打开一个TCP socket 连接,并在启动应用程序后等待AltUnity Driver的连接.
AltUnity Driver 模块用于连接到AltUnity Server,可以访问所有的Unity对象,并通过C#、Java、或Python编写的测试代码与它们交互。
AltUnity Tester Editor Window是用来检测Unity应用并直接从Unity编辑器运行c#测试代码的GUI。


开始

导入AltUnity Tester package 到Unity编辑器中
下载地址:
导入成功则可以打开如下工具栏界面(此为官网截图,新版本略有不同)。


在目标平台或者Unity编辑器中运行项目(此处仅说明Unity Editor和Standalone两种平台)
为了使测试代码能够通过AltUnity Driver 访问Unity对象,在运行测试代码之前,你需要启动配置了AltUnity Server 的项目。启动时,你的项目应该会显示一个弹出的信息"waiting for connection on port 13000".


使用AltUnity Tester Editor 发布或者启动的项目都会自动添加AltUnityRunnerPrefab的对象,包含TCP通讯相关功能
Unity Editor/PC

  • 以此顺序 Unity Editor -> AltUnity Tools -> AltUnityTester,打开AltUnity Tester窗口.



  • 在SceneMananger中选择测试相关的场景,初始化界面时自动添加Build Settings中的场景列表.
  • 在平台选择Editor/Standalone选项
  • 在Test list 选择要执行的测试用例



  • 点击Play in Editor/Build Only
当构建平台时Standalone时,确保Player Settings 中Api Compatibility Level设置未“.Net 4x”.通过以下操作顺序可以找到此配置 Edit menu -> Project Settings -> Player -> Other Settings -> Configuration.
为你的项目编写和执行第一个测试用例(c#)
AltUnity C# Driver 已经包含在AltUnity Tester package中,如果你用C#编写测试,那么你可以直接从Unity创建你的测试。步骤如下:

  • 在Unity 项目中创建Editor文件夹(如果不存在的话)
  • 在Editor文件夹中右键选择Create->AltUnityTest.这将创建一个模板文件,你可以在其中开始编写你的测试。
  • 打开AltUnity Tester窗口。
  • 在 Run Tests部分按 "Run All Tests "按钮。你应该在Unity Editor Console中看到测试的输出。
常用函数
FindObject

找到场景中第一个符合给定条件的对象。
参数
NameTypeDescription
byBy查找对象的条件类型
valuestring查找对象的匹配值
cameraByBy查找camera的条件类型
cameraNamestring场景中的所有摄像机将与该值进行比较,看它们是否符合标准,以获得计算物体屏幕坐标的摄像机。如果没有给定摄像机,它将搜索场景中的所有摄像机,直到某个摄像机看到该物体,或者返回计算出的该物体的屏幕坐标给场景中的最后一个摄像机。
enabledboolean如果为true,将只匹配hierarchy中active的对象。如果是false,将匹配所有对象。
[Test]
public void TestFindElement()
{
    const string name = "Capsule";
    var altElement = altUnityDriver.FindObject(By.NAME,name);
    Assert.NotNull(altElement);
    Assert.AreEqual(name, altElement.name);
}
By
它被用于查找对象的方法中,以设置对象被搜索的标准。目前有7种类型。

  • By.TAG - 搜索具有特定tag的对象
  • By.LAYER - 搜索具有特定layer的对象
  • By.NAME - 搜索以某种方式命名的对象(基于Unity对象名称的层次结构)
  • By.COMPONENT -  搜索具有特定component的对象
  • By.ID - 搜索分配了某个id的对象(每个对象都有一个唯一的id,所以这个标准总是会返回1或0个对象)。Id检查Instance Id和AltId(需要在对象上挂载AltUnityId的脚本,使用System.Guid.NewGuid生成AltId)
  • By.TEXT - 搜索有某种text文本内容的对象
  • By.PATH - 搜索符合一定路径的对象
按路径搜索对象--实现了下列选择节点和属性

  • object - 选择名称为“object”的所有对象
  • / - 从根节点选择
  • // -  从当前路径节点开始选择文档中符合选择条件的节点
  • .. - 选择当前节点的父节点
  • * - 匹配任何元素节点
  • contains - 选择名称中包含特定字符串的对象
  • [n-th] - 选择当前节点的第 n 个子节点。 0 - 代表第一个子节点,1 - 是第二个子节点,依此类推。 -1 - 代表最后一个子节点
  • @tag
  • @layer
  • @name
  • @component
  • @id
  • @text
//NameOfParent/NameOfChild/*

//NameOfParent/NameOfChild//*

altUnityDriver.FindObjects(By.PATH, "//Canvas/Panel/*")
从Panel 返回所有的直接子项

altUnityDriver.FindObjects(By.PATH, "//Canvas/Panel//*")
从Panel 返回所有的子项

//CapsuleInfo/..

altUnityDriver.FindObject(By.PATH, "//CapsuleInfo/..")
返回对象 CapsuleInfo 的父级

//NameOfParent/NameOfChild/*[@tag=tagName]

altUnityDriver.FindObjects(By.PATH, "//Canvas/Panel/*[@tag=UI]")
返回Panel对象下tag = UI的所有直接子项

//NameOfParent/NameOfChild/*[@layer=layerName]

altUnityDriver.FindObjects(By.PATH, "//Canvas/Panel/*[@layer=UI]")
返回Panel对象下layer = UI的所有直接子项

//NameOfParent/NameOfChild/*[@id=idMethod]

altUnityDriver.FindObject(By.PATH, "//*[@id=8756]")
返回 id = 8756 的对象

//NameOfParent/NameOfChild/*[@text=textName]

altUnityDriver.FindObject(By.PATH, "//Canvas/Panel//*[@text=Start]")
返回Panel对象下text= Start的第一个子项

//NameOfParent/NameOfChild/*[contains(@name,name)]

//NameOfParent/NameOfChild/*[contains(@text,text)]

altUnityDriver.FindObjects(By.PATH, "//*[contains(@name,Cub)]")
返回名称中包含字符串“Cub”的每个对象

//NameOfParent/NameOfChild/*[@selector1=selectorName1][@selector2=selectorName2][@selector3=selectorName3]

altUnityDriver.FindObject(By.PATH, "//Canvas/Panel/*[@component=Button][@tag=Untagged][@layer=UI
返回Panel下第一个tag = Untagged & layer = UI & 具有Button 组件的对象

//NameOfParent/NameObject

altUnityDriver.FindObjects(By.PATH, "/Canvas//Button[@component=ButtonLogic]"
返回 Canvas 中作为根对象并具有名为 ButtonLogic 的组件的所有按钮
//NameOfParent[n]

//NameOfParent/NameOfChild[n]

altUnityDriver.FindObject(By.PATH, "//Canvas[5]")
返回根节点 Canvas 的第 6 个直接子级

altUnityDriver.FindObject(By.PATH, "//Canvas/Panel/*[@tag=Player][-1]")
返回  Panel 下 tag = Player 的最后一个直接子级

WaitForObject
等待直到找到符合给定标准的对象或直到达到超时限制。
NameTypeDescription
byBy查找对象的条件类型
valuestring查找对象的匹配值
cameraByBy查找camera的条件类型
cameraNamestring场景中的所有摄像机将与该值进行比较,看它们是否符合标准,以获得计算物体屏幕坐标的摄像机。如果没有给定摄像机,它将搜索场景中的所有摄像机,直到某个摄像机看到该物体,或者返回计算出的该物体的屏幕坐标给场景中的最后一个摄像机。
enabledboolean如果为true,将只匹配hierarchy中active的对象。如果是false,将匹配所有对象。
timeoutdouble它将等待对象的秒数
intervaldouble检索对象的间隔时间,应小于超时时间
[Test]
public void TestWaitForObjectToNotExistFail()
{
     try
     {
         altUnityDriver.WaitForObjectNotBePresent(By.NAME,"Capsule", timeout: 1, interval: 0.5f);
         Assert.Fail();
     }
     catch (WaitTimeOutException exception)
     {
         Assert.AreEqual("Element //Capsule still found after 1 seconds", exception.Message);
     }
}

GetComponentProperty
返回给定组件属性的值。
NameTypeDescription
componentNamestring组件的名称。如果该组件有一个命名空间,其格式应该是这样的"namespace.componentName"
propertyNamestring你想要的属性值的名称。如果属性是一个数组,你可以通过property[index]来指定返回数组中的哪个元素,或者如果你想要一个属性在另一个属性中,你可以通过property.property2来获得,例如position.x。私有的private 字段亦可以获取。
assemblyNamestring包含组件的程序集的名称
maxDepthint设置该属性的序列化深度。例如,对于Transform中的position属性,结果如下:

maxDepth=2 {"normalized":{"magnitude":1.0, "sqrMagnitude":1.0, "x":0.871575534, "y":0.490261227, "z":0. 0},"magnitude":1101.45361,"sqrMagnitude":1213200.0,"x":960.0,"y":540.0,"z":0.0}  

maxDepth=1 :{"normalized":{},"magnitude":1101.45361,"sqrMagnitude":1213200.0,"x":960.0,"y":540.0,"z":0.0}

[Test]
public void TestGetComponentProperty()
{
    const string componentName = "AltUnityRunner";
    const string propertyName = "SocketPortNumber";
    var altElement = altUnityDriver.FindObject(By.NAME,"AltUnityRunnerPrefab");
    Assert.NotNull(altElement);
    var propertyValue = altElement.GetComponentProperty(componentName, propertyName);
    Assert.AreEqual(propertyValue, "13000");
}

Swipe
模拟游戏中的滑动动作。此命令不等待操作完成。需要等待操作完成,请使用 SwipeAndWait。
NameTypeDescription
startAltUnityVector2(C#)滑动的起始位置
endAltUnityVector2(C#)滑动结束位置
durationfloat将鼠标从当前位置移动到设置位置的时间(以秒为单位)。
[Test]
public void MultipleDragAndDrop()
{
    var altElement1 = altUnityDriver.FindObject(By.NAME,"Drag Image1");
    var altElement2 = altUnityDriver.FindObject(By.NAME,"Drop Box1");
    altUnityDriver.Swipe(new AltUnityVector2(altElement1.x, altElement1.y), new AltUnityVector2(altElement2.x, altElement2.y), 1);

    altElement1 = altUnityDriver.FindObject(By.NAME,"Drag Image2");
    altElement2 = altUnityDriver.FindObject(By.NAME,"Drop Box2");
    altUnityDriver.Swipe(new AltUnityVector2(altElement1.x, altElement1.y), new AltUnityVector2(altElement2.x, altElement2.y), 2);

    altElement1 = altUnityDriver.FindObject(By.NAME,"Drag Image3");
    altElement2 = altUnityDriver.FindObject(By.NAME,"Drop Box1");
    altUnityDriver.Swipe(new AltUnityVector2(altElement1.x, altElement1.y), new AltUnityVector2(altElement2.x, altElement2.y), 2);


    altElement1 = altUnityDriver.FindObject(By.NAME,"Drag Image1");
    altElement2 = altUnityDriver.FindObject(By.NAME,"Drop Box1");
    altUnityDriver.Swipe(new AltUnityVector2(altElement1.x, altElement1.y), new AltUnityVector2(altElement2.x, altElement2.y), 3);

    Thread.Sleep(4000);

    var imageSource = altUnityDriver.FindObject(By.NAME,"Drag Image1").GetComponentProperty("UnityEngine.UI.Image", "sprite");
    var imageSourceDropZone= altUnityDriver.FindObject(By.NAME,"Drop Image").GetComponentProperty("UnityEngine.UI.Image", "sprite");
    Assert.AreNotEqual(imageSource, imageSourceDropZone);

     imageSource = altUnityDriver.FindObject(By.NAME,"Drag Image2").GetComponentProperty("UnityEngine.UI.Image", "sprite");
     imageSourceDropZone = altUnityDriver.FindObject(By.NAME,"Drop").GetComponentProperty("UnityEngine.UI.Image", "sprite");
    Assert.AreNotEqual(imageSource, imageSourceDropZone);

}
参考资料:

https://www.zhihu.com/question/28729261
单元测试到底是什么?应该怎么做?
自动化测试与手动测试有什么区别? - 云+社区 - 腾讯云
欢迎使用 - Airtest Project Docs
https://github.com/AirtestProject/Poco
https://github.com/tencent/GAutomator
https://www.raywenderlich.com/9454-introduction-to-unity-unit-testing
AltUnity Tester - AltUnity Tester Documentation

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-11 01:30 , Processed in 0.445754 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表