游戏测试-战斗内核的自动化测试

1,177 阅读4分钟

本文首发于:行者AI

今天给大家分享一个游戏自动化测试的落地。这款游戏有独立的战斗内核负责局内战斗的计算,所以每次需要测试战斗内核时,都需要服务器重新部署,客户端(移动端、PC端等)重新出包,最后才能交付给测试进行测试,整个流程比较长,也比较耗时,所以我们就考虑在战斗内核更新时就进行测试,这样可以简化测试流程,节约时间。

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

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,
client.setRound.4.0,
2,
client.addChessToMap.0.110011.0.0,
client.goNextStage,
sleep.4.5,
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 gitlab
import os
import time
curPath = 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;

如果你还发现了其他的问题,欢迎在评论区指出,也希望可以和对游戏自动化测试感兴趣的朋友一起交流游戏自动化测试相关的方法和经验。


PS:更多技术干货,快关注【公众号 | xingzhe_ai】,与行者一起讨论吧!