本文首发于:行者AI

明天给大家分享一个游戏自动化测试的落地。这款游戏有独立的战斗内核负责局内战斗的计算,所以每次须要测试战斗内核时,都须要服务器重新部署,客户端(挪动端、PC端等)从新出包,最初能力交付给测试进行测试,整个流程比拟长,也比拟耗时,所以咱们就思考在战斗内核更新时就进行测试,这样能够简化测试流程,节约工夫。

通过和内核组开发的探讨后,决定应用内核开发组提供的QT工具(如下图展现),在本地运行游戏的战斗内核,通过执行多个命令构建起测试的场景,再通过数据交互拿到测试数据,以此来达到咱们想要的测试目标。

1. 环境筹备

  • Windows10
  • Python3.7
  • Allure
  • Python第三方库:pytest、allure-pytest、python-gitlab、zipp、xlrd

2. 实现流程

2.1 通信形式

启动QT时,启动参数中蕴含通信的端口号。QT启动之后,应用python与QT在本地建设socket连贯进行通信。

    def __init__(self, port):        self.port = port        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)        self.data = {}    def connect(self):        """        建设连贯        """        localhost = socket.gethostbyname(socket.gethostname())        self.sock.connect({localhost, self.port})

将每条用例的操作命令寄存在列表中,遍历列表进行命令的发送,每条命令json序列化之后,通过socket连贯进行发送,发送实现后,对发送的命令进行判断,再做出下一步的操作。

    def send(self, data):        """        发送命令        """        for d in data:            if 'sleep' == d.keys():                # 放弃socket连贯,向QT发送心跳数据                self.hearbeats(int(d['sleep'][0]), int(d['sleep'][1]))            else:                _ = json.dumps(d).encode(encoding='utf-8')                self.sock.send(_)                # 多条命令发送须要距离0.3s                time.sleep(0.3)                # 发送初始化命令之后,期待QT数据初始化                if d['Pack-Field'] == 'client.initserver':                    time.sleep(5)                # 当Hread-Field==2时示意向内核申请以后游戏数据                if d['Hread-Field'] == '2':                    # 8个玩家数据+1个游戏场景数据                    self.data = [self.recv_game_data() for _ in range(9)]

接收数据时,前8个字节为包的大小,前面则为咱们须要的数据,因为是在本地进行的测试,所以没有对数据进行加密,接管到的数据能够间接转成json格局,在接收数据时会收到QT发过来的心跳数据,将心跳数据去除掉之后,就是咱们想要的游戏数据了。

    def recv_game_data(self):        """        接管游戏数据,去除心跳数据        """        data = self.recv_data()        # 当Hread-Field==Heartbeat时示意数据为心跳数据        while data['Hread-Field'] == 'Heartbeat':            data = self.recv_data()        else:            return data    def recv_data(self):        size = struct.unpack('q', self.recv(8))[0]        data = json.loads(self.recv(size).decode('utf-8'))        return data    def recv(self, size):        n = 0        data = b''        while n < size:            _ = self.sock.recv(size - n)            data += _            n += len(_)

2.2 用例设计

将用例对立寄存在Excel文件中,每个Sheet为一个游戏模式,每张表都蕴含了用例编号、形容、状态、步骤、数据校验。用例执行时,依照事后设计好的步骤向QT发送命令构建测试场景,接管到QT返回的数据之后,再与数据校验中的数据进行比对,以此来校验内核性能的正确性。

用例编号用例形容用例状态测试步骤数据校验
根底金币-001对局获胜,胜利金币+1,根底金币+5执行client.initserver,<br/>client.setRound.4.0,<br/>2,<br/>client.addChessToMap.0.110011.0.0,<br/>client.goNextStage,<br/>sleep.4.5,<br/>2{"data_2":{"player_0":{"gold_increase":6},"GameMode":0}}

2.3 数据校验

QT每次返回的数据中都蕴含了8个玩家数据以及1个游戏场景数据,所以咱们依照用例中申明的校验字段,提取返回数据中绝对应的字段进行比照校验,具体的实现代码如下:

    def format_data(self):        """        格式化接管到的数据        """        for _ in self.received_data.keys():            data = self.received_data[_]            self.received_data_formatted[_] = dict()            for player_num in range(len(data)):                player_data = data[player_num]                if player_num == 8:                    self.received_data_formatted.get(_)['game_scene'] = player_data                else:                    self.received_data_formatted.get(_)[f'player_{player_num}'] = player_data        def verify_data(self):        """        进行数据校验        """        self.format_data()        for data_key in self.assert_data.keys():            self.data_key = data_key            for check_type_1 in self.assert_data[data_key].keys():                self.check_type = check_type_1                if check_type_1 == 'GameMode':                    try:                        self.GameMode(data_key)                    except AssertionError as e:                        self.failed_dict['GameMode'] = e.args                else:                    for check_type_2 in self.assert_data[data_key][check_type_1]:                        try:                            # 执行对应的校验模块                            eval(f'self.{check_type_2}()')                        except AssertionError as e:                            # 捕捉校验模块返回的异样并记录                            self.failed_dict[check_type_2] = e.args

数据校验中的字段如gold_increase、GameMode都对应一个校验的模块,gold_increase指玩家以后金币的减少数量,GameMode指以后的游戏模式,当然还有很多校验模块如:HandleChess、HandleEquip、ChessBoard、BoardEquip...等等

    def gold_increase(self):        # 校验金币增加值        assert_data = self.assert_data[self.data_key][self.check_type]['gold_increase']        received_data_1 = self.received_data_formatted[f'data_{int(self.data_key[-1:]) - 1}'][self.check_type]['Pack-Field']['k_19']        received_data_2 = self.received_data_formatted[self.data_key][self.check_type]['Pack-Field']['k_19']        increase_num = received_data_2-received_data_1        if increase_num != int(assert_data):            raise AssertionError(f'{self.data_key}-{self.check_type}: {increase_num} != {assert_data}')        def GameMode(self, data_key):    # 校验以后游戏模式    game_mode_assert = self.assert_data[data_key]['GameMode']    game_mode_received = self.received_data_formatted.get(data_key)['game_scene']['Pack-Field']['k_0']    assert game_mode_received == game_mode_assert,\        f'{self.data_key}-{self.check_type}: {game_mode_received} != {game_mode_assert}'

2.4 内核版本的更新

应用python-gitlab库下载gitlab上指定内核分支中的文件,下载实现后替换掉旧的文件即可,以此来更新本地的内核版本,装置python-gitlab库。

pip install python-gitlab
import gitlabimport osimport timecurPath = os.path.abspath(os.path.dirname(__file__))rootPath = os.path.split(curPath)[0]class DownloadFiles:    """    从gitlab上下载指定分支文件    """    def __init__(self, version):        self.dir_name = None        self.version = version    def create_dir(self):        if not os.path.isdir(self.dir_name):            os.makedirs(self.dir_name)            time.sleep(0.1)    def start_get(self):        gl = gitlab.Gitlab.from_config('xiaoming', [f'{curPath}/git.ini'])        gl.projects.list()        project = gl.projects.get(1234) # 我的项目ID        root_path = f'{rootPath}/resource/'        info = project.repository_tree(all=True, recursive=True, as_list=True, ref=self.version)        file_list = list()        if not os.path.isdir(root_path):            os.makedirs(root_path)        os.chdir(root_path)        for info_dir in range(len(info)):            if info[info_dir]['type'] == 'tree':                self.dir_name = info[info_dir]['path']                self.create_dir()            else:                file_name = info[info_dir]['path']                # logger.info(file_name)                if file_name == 'windows.zip':                    file_list.append(file_name)                if 'Datas_jit' in file_name:                    file_list.append(file_name)        if 'Datas_jit/' not in str(file_list):            # raise ValueError("未检测到Datas相干文件,请上传")            pass        for info_file in range(len(file_list)):            get_file = project.files.get(file_path=file_list[info_file], ref=self.version)            content = get_file.decode()            with open(file_list[info_file], 'wb') as code:                logger.info(f"START-DOWNLOAD: {file_list[info_file]}")                code.write(content)        logger.info(f"DOWNLOAD COMPLETE!")

3. 不足之处

以后内核自动化测试还存在着一些不足之处:

  • 用例的编写比较复杂,首先须要相熟命令,其次是校验的字段名,对数据校验的格局也有要求;
  • 以后版本只能在Windows环境中运行,如果能够间接在测试服上运行是最好的;
  • 如果C++的代码有改变的话,就须要手动更新QT;

如果你还发现了其余的问题,欢送在评论区指出,也心愿能够和对游戏自动化测试感兴趣的敌人一起交换游戏自动化测试相干的办法和教训。