Case书写

你可以使用小游戏测试SDK实现自动化Case,实现选取节点,节点交互(如点击,滑动等)等功能。

总览

小游戏自动化测试SDK和一般的前端自动化测试框架结构类似。通过继承名为BaseCase的基类,实现run方法。run方法会自动传入Context对象,Context对象提供各种各样的API供开发者使用。

示例

JavaScript版本:

const mini_game_test = require("mini_game_test")
const BaseCase = mini_game_test.BaseCase
const logger = mini_game_test.logger

class Test extends BaseCase {
    async run(c) {
        logger.info("start")

        let tmp = null
        //获取节点
        tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=2|nodeType=laya.display.Sprite]}>{laya.display.Sprite[index=2|nodeType=laya.display.Sprite|covered=false]}")
        //点击
        await tmp.tap()
        //等待
        await c.sleep(3000)
        let endStatus = false
        for (let i = 0; i < 10; i++) {
            await c.tap(0.5, 0.5)
            await c.sleep(2000)
            tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=5|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=3|nodeType=laya.display.Sprite|covered=false]}")
            if (!tmp.isEmpty()) {
                logger.info("game end")
                endStatus = true
                break
            }
        }

        if (!endStatus) {
            logger.info("game not end or game error")
            return
        }

        tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=5|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=3|nodeType=laya.display.Sprite|covered=false]}")
        await tmp.tap()
        await c.sleep(3000)
        tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=6|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}")
        await tmp.tap()
        await c.sleep(3000)
        tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=5|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=4|nodeType=laya.display.Sprite|covered=false]}")
        await tmp.tap()
        await c.sleep(3000)
        tmp = await c.findPath("{laya.display.Stage[nodeType=laya.display.Stage|covered=false]}>{laya.display.Sprite[index=0|nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[nodeType=laya.display.Sprite|covered=false]}>{laya.display.Sprite[index=7|nodeType=laya.display.Sprite|covered=false]}")
        await tmp.tap()
        await c.sleep(3000)
    }
}

logger.debug("start")

const t = new Test()
t.start()

Python版本:

# -*- coding: utf-8 -*-
from mini_game_test_case_py.case.BaseCase import BaseCase
from mini_game_test_case_py.lib import config
from mini_game_test_case_py.lib import Asserts
from mini_game_test_case_py.lib import Logger
import time


class TestCase(BaseCase):
    def run(self, c):
        Logger.info("start case")
        #获取开始按钮
        start_node = c.find_path(
            "{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite}>{laya.display.Sprite[index=2|covered=false]}")
        #判断开始按钮是否存在
        Asserts.assert_nodeselector_exists(start_node)
        #点击
        start_node.tap()
        c.sleep(6000)

        start_time = time.time()
        
        endStatus = False
		
        #循环判断paihang_node是否出现
        while time.time() - start_time < 30  :
            c.tap(0.5, 0.5, is_per=True)
            time.sleep(1)
            paihang_node = c.find_path("{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite[index=5|covered=false]}>{laya.display.Sprite[index=3|covered=false]}")
            if not paihang_node.is_empty():
                Logger.info("game end")
                endStatus = True
                break

        if not endStatus:
            Logger.info("game end error")
            return
        
        tmp = c.find_path("{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite[index=5|covered=false]}>{laya.display.Sprite[index=3|covered=false]}")
        Asserts.assert_nodeselector_exists(tmp)
        tmp.tap()
        c.sleep(6000)
        tmp = c.find_path("{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite[index=6|covered=false]}>{laya.display.Sprite[index=0|covered=false]}")
        Asserts.assert_nodeselector_exists(tmp)
        tmp.tap()
        c.sleep(6000)
        tmp = c.find_path("{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite[index=5|covered=false]}>{laya.display.Sprite[index=4|covered=false]}")
        Asserts.assert_nodeselector_exists(tmp)
        tmp.tap()
        c.sleep(6000)
        tmp = c.find_path("{laya.display.Stage[covered=false]}>{laya.display.Sprite[index=0|covered=false]}>{laya.display.Sprite[covered=false]}>{laya.display.Sprite[index=7|covered=false]}")
        Asserts.assert_nodeselector_exists(tmp)
        tmp.tap()
        c.sleep(6000)        
        Logger.info("end test")



t = TestCase()
t.start()

节点选择

节点选择支持Path或者链式调用以及图像识别

Path或链式调用

通过Path或者链式调用来选择对应节点,Path的格式为:

{节点的className[节点属性=目标值|.......]}>或者.{节点的className[节点属性=目标值|.......]}.....

Path中节点由{}包围,className作为节点的名称,后方[]中包括节点的额外选择条件。额外选择条件可不填写,条件格式为k=v,多个条件之间用|连接,举例:

{a[k1=v1|k2=v2]}

该Path会选择className=a,同时满足属性k1=v1以及k2=v2的节点。

多个节点之间支持由**.或者>**连接,它们分别表示:

  • . 为选择当前节点的后代节点,即当前节点的所有后代节点都会作为备选。

  • > 为仅选择当前节点的子节点。

    举例:

    {a}.{b}>{c}
    

Path可以调用findPath方法传入调用,获取对应的NodeSelector实例对象。CaseContext实例对象或者NodeSelector实例对象都可以调用findPath方法,返回值为NodeSelector实例对象,如:

tmp = await c.findPath("{a}.{b}>{c}") //JavaScript
tmp = c.find_path("{a}.{b}>{c}") #python

如果不想使用Path选择节点,还可以使用链式调用:

tmp = await c.select((n)=>{ return (n["a"] || "") === "b" }).child({"c" : "d"}) //JavaScript
tmp = c.select(lambda n: n.get("a",None) == "b").child({"c" : "d"}) #python

注意,JavaScript版本中 CaseContext实例对象的findPath方法以及select、child方法都是async function,调用时需要通过await 获取结果

Tips

下面以JavaScript版本为样例介绍一下书写Case的技巧(Python版本通用)。

  • 目标的节点Path可以通过Game Inspector插件获取,如:

Path

这个是插件建议的Path,并不适应所有的情况。例如有些游戏在同一个场景中,树的结构可能是不稳定的,如index可能并不固定。所以当出现这种情况的时候,建议酌情更改属性选择条件。

  • 因为Path选择的是所有满足条件的节点,所以可以将中间一些无用的节点关系删去,使用后代选择的方式。即Path中的.连接节点以及链式调用中的select funciton,可以有效缩短Path长度,如:

    {cc.Scene[name=TestList|nodeType=cc.Scene_]}.{cc.Node[index=3|name=btnRestart|nodeType=cc.Node_Sprite:Widget:Button:|covered=false]}>{cc.Node[index=0|name=label|nodeType=cc.Node_Label:]}
    
  • 调用findPath,Path第一个节点默认使用后代选择方式,即如果某个节点的某个属性可以唯一区分,可以直接选择,如:

    c.findPath("{a[name='quit']}") //JavaScript
    
  • 有一些属性的区分度相对较高,如:

    1. covered 该属性为false代表未被事件遮盖,即用户可以与该节点产生交互且有事件监听,这种节点一般都是主要交互目标。
    2. name 以及 nodeType。如果开发者设置了该属性,且区分度比较高,则可以尝试使用这些属性进行筛选。
  • index属性代表节点在父节点children list中所处的位置,从0开始计算。

  • 有时候只凭借上下级的className可以快速的定位目标节点,如:

    {a}.{b}>{c}>{d}
    
图像识别

SDK支持直接通过识别目标图片,获取其位置,进行交互,JavaScript版本调用方式如下:

let imgPath = path.join("img","1.png") //为了兼容跨平台,请使用系统库生成相对路径
tmp = await c.findImg(imgPath)

Python版本如下:

img_path = os.path.join("img","1.png") #为了兼容跨平台,请使用系统库生成相对路径
tmp = c.find_img(img_path)

目标图片的路径,是基于测试Case路径的。如果目标图片的绝对路径为 ~/{workspace}/img/1.png的话,则findImg传入的imgPath则为img/1.png。

findImg函数返回的是ImgSelector实例对象,可以使用该对象对目标图片位置进行操作。

Tips
  • 传入的imgPath由于跨平台原因,最好使用path.join 方式进行连接。
  • 图片识别可能会有误差,如有问题请反馈给我们,谢谢。

事件交互

SDK目前支持点击,滑动操作,更多的操作正在开发中。

通过调用ImgSelector实例对象以及NodeSelector实例对象的tap或者swipe方法,触发对应操作。每次操作之后,建议考虑游戏具体场景适当等待。

如果搜索节点时,未找到目标节点,则返回的ImgSelector实例对象或NodeSelector实例对象内容为空,这时触发事件会产生error。建议如果无法确定是否能获取到节点的时候,通过isEmpty()判断是否为空。

如果满足条件的节点有多个,则NodeSelector实例对象也会包括多个节点对象。这时触发事件,则会产生error。如果出现这种问题,建议调整选择条件。