静态测试或保存Private Ryan

发行版通常会被忽视。 在他面前突然发现的任何错误都威胁着我们的时限,修复程序,工作到早晨以及紧张的时刻。 当这样的匆忙开始系统地发生时,我们意识到您已经不能再这样生活了。 决定开发一个全面的验证系统来挽救普通的Ryan开发人员Artyom,后者在晚上9点,10点或11点发布之前就回家了。 开发人员的想法是让开发人员找出错误,而更改尚未到达存储库,而他本人也没有丢失任务的上下文。


如今,首先在本地仔细检查所做的更改,然后在装配场进行一系列集成测试。 在本文中,我们将讨论验证的第一阶段-静态测试,它监视资源的正确性并分析代码。 这是链中的第一个子系统,它解决了发现的大部分错误。

一切如何开始


在发行前一周半的质量检查开始了手动检查发行前游戏的过程。 自然,需要尽快修复此阶段的错误。

由于缺乏时间来寻求一个好的解决方案,因此添加了一个临时的“拐杖”,该拐杖已经扎根很长时间,并被其他不太受欢迎的解决方案所包围。

首先,我们决定自动发现明显的错误:崩溃,无法完成游戏的主要操作(开设商店,进行购买,玩游戏)。 为此,游戏以特殊的自动游戏模式开始,如果出现问题,则在通过我们的农场测试后我们将立即知道。

但是测试人员和我们的自动烟雾测试都发现的大多数错误是缺少资源或不同系统的设置不正确。 因此,下一步是静态测试 -在不启动应用程序的情况下检查资源的可用性,它们之间的关系和设置。 该系统是在装配场的另一步骤中启动的,大大简化了错误的查找和修复。 但是,如果即使在提交问题代码并将其放入存储库之前仍能检测到错误,为什么还要浪费汇编场的资源呢? 这可以通过precommit hooks完成 ,它们在创建提交并将其发送到存储库之前刚刚启动。

是的,我们是如此的酷,以至于提交之前和在组装场上的静态测试都由一个代码执行,这大大简化了对它的支持。

我们的努力可以分为三个领域:

  • 创建一个装配场-收集所有经过检查的物品的地方;
  • 开发静态测试-检查资源的正确性,它们之间的关系,启动代码分析器;
  • 开发运行时测试-以自动播放模式启动应用程序。

另一个任务是组织开发人员在计算机上启动测试。 必须尽量减少本地执行时间(开发人员不必等待10分钟来提交一行),并确保进行更改的每个系统都安装了我们的系统。

许多要求-一个系统


在开发过程中,可以使用一整套程序集:带或不带作弊功能,beta或alpha版本,iOS或Android。 在每种情况下,可能需要不同的资源,设置甚至不同的代码。 为每个可能的组件编写用于静态测试的脚本会导致具有许多参数的复杂系统。 除了难以维护和修改之外,每个项目还具有自己的一套拐杖自行车。

通过反复试验,我们来到了一个系统,每个测试都可以考虑启动上下文,并决定是否运行它,确切地检查什么以及如何检查。 在测试开始时,我们确定了三个主要属性:

  • 程序集的类型:对于发布和调试资源,检查的严格性,覆盖范围的完整性以及标识符的设置和可用功能的验证将有所不同;
  • 平台:适用于android的内容可能不适用于iOS,资源的收集方式也不同,并非android版本中的所有资源都将位于iOS中,反之亦然;
  • 启动位置:我们确切的启动位置-在需要所有可用测试的构建代理上,或在需要最小化启动列表的用户计算机上。


静态测试系统


系统的核心和基本的静态测试集是在python中实现的。 基础仅是少数几个实体:


测试上下文是一个广泛的概念。 它既存储了我们上面讨论过的build和launch参数,又存储了测试填充和使用的元信息。

首先,您需要了解要运行哪些测试。 为此,元信息包含我们对此发布特别感兴趣的资源类型。 资源类型由系统中注册的测试确定。 可以将一个测试与一个或多个类型“关联”,并且如果在提交时发现该测试检查的文件已更改,则关联的资源也已更改。 这很方便地适合我们的思想-在本地运行尽可能少的检查:如果负责测试的文件没有更改,则无需运行它。

例如,有一条鱼的描述,其中指示了3D模型和纹理,如果描述文件已更改,则检查其中所指示的模型和纹理是否存在。 在其他情况下,无需进行鱼检查。

另一方面,更改资源可能需要更改和依赖于它的实体:如果我们存储在xml文件中的纹理集已更改,则有必要检查其他3D模型,因为可能会发现您需要的纹理已被删除。 上述优化仅在提交时在用户计算机上本地应用,并且在组装场中启动时,假定所有文件均已更改,并且我们运行所有测试。

下一个问题是某些测试对其他测试的依赖:在找到所有纹理和模型之前不可能检查鱼。 因此,我们将所有执行分为两个阶段:

  • 语境准备
  • 检查

在第一阶段,上下文中充满了有关找到的资源的信息(对于鱼,具有图案和纹理的标识符)。 在第二阶段,使用存储的信息,只需检查所需资源是否存在。 下面提供了简化的上下文。

class VerificationContext(object):   def __init__(self, app_path, build_type, platform, changed_files=None):       self.__app_path = app_path       self.__build_type = build_type       self.__platform = platform       #          self.__modified_resources = set()       self.__expected_resources = set()       #      ,              self.__changed_files = changed_files       # -  ,          self.__resources = {} def expect_resources(self, resources):   self.__expected_resources.update(resources) def is_resource_expected(self, resource):   return resource in self.__expected_resources def register_resource(self, resource_type, resource_id, resource_data=None):   self.__resources.setdefault(resource_type, {})[resource_id] = resource_data def get_resource(self, resource_type, resource_id):   if resource_type not in self.__resources or resource_id not in self.__resources[resource_type]:       return None, None   return resource_id, self.__resources[resource_type][resource_id] 

确定了影响测试启动的所有参数后,我们设法将所有逻辑隐藏在基类中。 在特定的测试中,仅写测试本身和参数的必要值仍然是必要的。

 class TestCase(object):  def __init__(self, name, context, build_types=None, platforms=None, predicate=None,               expected_resources=None, modified_resources=None):      self.__name = name      self.__context = context      self.__build_types = build_types      self.__platforms = platforms      self.__predicate = predicate      self.__expected_resources = expected_resources      self.__modified_resources = modified_resources      #               #   ,          self.__need_run = self.__check_run()      self.__need_resource_run = False  @property  def context(self):      return self.__context  def fail(self, message):      print('Fail: {}'.format(message))  def __check_run(self):      build_success = self.__build_types is None or self.__context.build_type in self.__build_types      platform_success = self.__platforms is None or self.__context.platform in self.__platforms      hook_success = build_success      if build_success and self.__context.is_build('hook') and self.__predicate:          hook_success = any(self.__predicate(changed_file) for changed_file in self.__context.changed_files)      return build_success and platform_success and hook_success  def __set_context_resources(self):      if not self.__need_run:          return      if self.__modified_resources:          self.__context.modify_resources(self.__modified_resources)      if self.__expected_resources:          self.__context.expect_resources(self.__expected_resources)   def init(self):      """        ,                    ,          """      self.__need_resource_run = self.__modified_resources and any(self.__context.is_resource_expected(resource) for resource in self.__modified_resources)  def _prepare_impl(self):      pass  def prepare(self):      if not self.__need_run and not self.__need_resource_run:          return      self._prepare_impl()  def _run_impl(self):      pass  def run(self):      if self.__need_run:          self._run_impl() 

回到鱼的例子,我们需要两个测试,一个测试找到纹理并将其注册到上下文中,另一个测试为找到的模型搜索纹理。

 class VerifyTexture(TestCase):  def __init__(self, context):      super(VerifyTexture, self).__init__('VerifyTexture', context,                                          build_types=['production', 'hook'],                                          platforms=['windows', 'ios'],                                          expected_resources=None,                                          modified_resources=['Texture'],                                          predicate=lambda file_path: os.path.splitext(file_path)[1] == '.png')  def _prepare_impl(self):      texture_dir = os.path.join(self.context.app_path, 'resources', 'textures')      for root, dirs, files in os.walk(texture_dir):          for tex_file in files:              self.context.register_resource('Texture', tex_file) class VerifyModels(TestCase):  def __init__(self, context):      super(VerifyModels, self).__init__('VerifyModels', context,                                         expected_resources=['Texture'],                                         predicate=lambda file_path: os.path.splitext(file_path)[1] == '.obj')  def _run_impl(self):      models_descriptions = etree.parse(os.path.join(self.context.app_path, 'resources', 'models.xml'))      for model_xml in models_descriptions.findall('.//Model'):          texture_id = model_xml.get('texture')          texture = self.context.get_resource('Texture', texture_id)          if texture is None:              self.fail('Texture for model {} was not found: {}'.format(model_xml.get('id'), texture_id)) 

项目传播


Playrix中的游戏开发是在其自己的引擎上进行的,因此,所有项目都使用相同的规则具有相似的文件结构和代码。 因此,有许多通用测试只能编写一次并在通用代码中。 项目足以更新测试系统的版本并将新的测试连接到其自身。

为了简化集成,我们编写了一个运行程序,该运行程序接收配置文件和设计测试(稍后将进行介绍)。 配置文件包含我们上面编写的基本信息:程序集的类型,平台,项目的路径。

 class Runner(object):  def __init__(self, config_str, setup_function):      self.__tests = []      config_parser = RawConfigParser()      config_parser.read_string(config_str)      app_path = config_parser.get('main', 'app_path')      build_type = config_parser.get('main', 'build_type')      platform = config_parser.get('main', 'platform')      '''      get_changed_files         CVS      '''      changed_files = None if build_type != 'hook' else get_changed_files()      self.__context = VerificationContext(app_path, build_type, platform, changed_files)      setup_function(self)  @property  def context(self):      return self.__context  def add_test(self, test):      self.__tests.append(test)  def run(self):      for test in self.__tests:          test.init()      for test in self.__tests:          test.prepare()      for test in self.__tests:          test.run() 

配置文件的优点在于,它可以在装配场中以自动模式为不同的装配生成。 但是通过此文件传递所有测试的设置可能不太方便。 为此,在项目存储库中存储了一个特殊的配置xml,并在其中写入了被忽略文件的列表,用于代码搜索的掩码等。

配置文件示例
 [main] app_path = {app_path} build_type = production platform = ios 

调整xml示例
 <root> <VerifySourceCodepage allow_utf8="true" allow_utf8Bom="false" autofix_path="ci/autofix"> <IgnoreFiles>*android/tmp/*</IgnoreFiles> </VerifySourceCodepage> <VerifyCodeStructures> <Checker name="NsStringConversion" /> <Checker name="LogConstructions" /> </VerifyCodeStructures> </root> 

除了一般部分外,项目还有其自身的特点和差异;因此,有一些项目测试是通过运行程序的配置连接到系统的。 对于示例中的代码,只需几行就可以运行:

 def setup(runner):  runner.add_test(VerifyTexture(runner.context))  runner.add_test(VerifyModels(runner.context)) def run():  raw_config = '''  [main]  app_path = {app_path}  build_type = production  platform = ios  '''  runner = Runner(raw_config, setup)  runner.run() 

收集耙


尽管python本身是跨平台的,但我们经常遇到这样的问题,即用户拥有自己的独特环境,在这种环境中,他们可能没有我们期望的版本,多个版本或根本没有解释器。 结果,它不能按我们预期的那样工作或根本不工作。 有几个迭代可以解决此问题:

  1. Python和所有软件包均由用户安装。 但是有两个“ buts”:并非所有用户都是程序员,并且通过pip install对设计人员进行安装,对于程序员也是如此。
  2. 有一个脚本可以安装所有必需的软件包。 这已经更好了,但是如果用户安装了错误的python,则工作中可能会发生冲突。
  3. 从工件存储(Nexus)提供正确的解释器版本和相关性,并在虚拟环境中运行测试。

另一个问题是性能。 测试次数越多,在用户计算机上检查更改的时间越长。 每隔几个月就会对瓶颈进行概要分析和优化。 因此改善了上下文,出现了文本文件缓存,改进了谓词机制(确定此文件对于测试很有趣)。

然后剩下的只是解决如何在所有项目上实施该系统并迫使所有开发人员包括二手挂钩的问题,但这是一个完全不同的故事...

结论


在开发过程中,我们在耙子上跳舞,奋力拼搏,但是仍然得到了一个系统,该系统可以使我们在提交期间发现错误,减少测试人员的工作,并且在发布纹理缺失之前的任务已经成为过去。 为了获得完全的幸福,简单的环境设置和单个测试的优化是不够的,但是来自ci部门的go在这方面努力工作。

在本文的存储库中可以找到本文中用作示例的代码的完整示例。

Source: https://habr.com/ru/post/zh-CN452926/


All Articles