اختبار ثابت أو حفظ الجندي ريان

وغالبًا ما يزحف الإصدار دون أن يلاحظه أحد. وأي خطأ تم اكتشافه فجأة أمامه يهددنا بحدوث تحول في المواعيد النهائية ، والإصلاحات ، والعمل حتى الصباح وقضاء الأعصاب. عندما بدأت هذه الذروة تحدث بشكل منتظم ، أدركنا أنه لا يمكنك العيش مثل هذا بعد الآن. تقرر تطوير نظام شامل للتحقق من الصحة لحفظ مطور Ryan العادي Artyom ، الذي عاد إلى المنزل قبل الإصدار الساعة 9 مساءً أو الساعة 10 أو 11 ... جيدًا ، كما تعلم. كانت الفكرة للمطور هو معرفة الخطأ ، في حين أن التغييرات لم تصل بعد إلى المستودع ، ولم يفقد هو نفسه سياق المهمة.


اليوم ، يتم فحص التغييرات التي تم إجراؤها بعناية أولاً محليًا ، ثم مع سلسلة من اختبارات التكامل في مزرعة التجميع. في هذه المقالة ، سنتحدث عن المرحلة الأولى من التحقق - اختبار ثابت ، والتي تراقب صحة الموارد وتحليل الكود. هذا هو النظام الفرعي الأول في السلسلة وهو يمثل الجزء الأكبر من الأخطاء التي تم العثور عليها.

كيف بدأ كل شيء


بدأت العملية اليدوية للتحقق من اللعبة قبل إصدارها في QA لمدة أسبوع ونصف قبل إطلاقها. بطبيعة الحال ، يجب إصلاح الأخطاء الموجودة في هذه المرحلة في أسرع وقت ممكن.

نظرًا لضيق الوقت لحل جيد ، تتم إضافة "عكاز" مؤقت ، والذي يأخذ جذرًا لفترة طويلة ويحيط به حلول أخرى غير شائعة.

بادئ ذي بدء ، قررنا أتمتة اكتشاف الأخطاء العلنية: الأعطال ، عدم القدرة على إكمال المجموعة الرئيسية من الإجراءات للعبة (فتح متجر ، إجراء عملية شراء ، لعب مستوى). للقيام بذلك ، تبدأ اللعبة في وضع لعبة autogame خاص ، وإذا حدث خطأ ما ، فسنعلم بذلك فور اجتياز الاختبار في مزرعتنا.

ولكن معظم الأخطاء التي وجدها كل من المختبرين واختبار الدخان الآلي الخاص بنا هي عدم وجود مورد أو إعدادات غير صحيحة لأنظمة مختلفة. لذلك ، كانت الخطوة التالية هي إجراء اختبار ثابت - التحقق من توفر الموارد وعلاقاتها وإعداداتها دون تشغيل التطبيق. تم إطلاق هذا النظام من خلال خطوة إضافية في مزرعة التجميع وتبسيطها إلى حد كبير في اكتشاف الأخطاء وإصلاحها. ولكن لماذا تهدر موارد مزرعة التجميع إذا كان يمكنك اكتشاف خطأ حتى قبل الالتزام والحصول على رمز المشكلة في المستودع؟ يمكن القيام بذلك عن طريق ربط خطاطيف مسبق ، والتي بدأت للتو قبل إنشاء الالتزام وإرسالها إلى المستودع.

ونعم ، نحن رائعون للغاية بحيث يتم إجراء اختبار ثابت قبل الالتزام وفي مزرعة التجميع بواسطة رمز واحد ، مما يبسط دعمه بشكل كبير.

يمكن تقسيم جهودنا إلى ثلاثة مجالات:

  • إنشاء مزرعة للتجميع - المكان الذي يتم فيه جمع كل شيء والتحقق منه هو نفسه ؛
  • تطوير اختبارات ثابتة - التحقق من صحة الموارد ، وعلاقاتها ، وإطلاق أجهزة تحليل الكود ؛
  • تطوير اختبارات وقت التشغيل - إطلاق التطبيق في وضع التشغيل التلقائي.

وكانت مهمة منفصلة لتنظيم إطلاق الاختبارات على الجهاز من قبل المطور. كان من الضروري تقليل وقت التنفيذ محليًا (لا يتعين على المطور الانتظار 10 دقائق للالتزام بسطر واحد) والتأكد من تثبيت نظامنا لكل نظام يقوم بإجراء التغييرات.

العديد من المتطلبات - نظام واحد


أثناء التطوير ، هناك مجموعة كاملة من المجموعات التي يمكن أن تكون في متناول اليد: مع وبدون غش ، بيتا أو ألفا ، iOS أو Android. في كل حالة ، قد تكون هناك حاجة إلى موارد أو إعدادات أو حتى رموز مختلفة. تؤدي كتابة البرامج النصية للاختبارات الثابتة لكل مجموعة محتملة إلى نظام معقد به العديد من المعلمات. بالإضافة إلى صعوبة صيانته وتعديله ، يحتوي كل مشروع أيضًا على مجموعته الخاصة من الدراجات المزدحمة.

من خلال التجربة والخطأ ، توصلنا إلى نظام واحد ، كل اختبار يمكن أن يأخذ في الاعتبار سياق الإطلاق ويقرر ما إذا كان سيتم تشغيله أم لا ، وما هو بالضبط وكيفية التحقق منه. في بداية الاختبارات ، حددنا ثلاث خصائص رئيسية:

  • نوع التجميع: بالنسبة لموارد الإصدار والتصحيح ، ستختلف عمليات الفحص من حيث الدقة ، واكتمال التغطية ، وكذلك إعدادات المعرفات والتحقق من الوظائف المتاحة ؛
  • النظام الأساسي: ما هو صحيح بالنسبة إلى android قد يكون غير صحيح بالنسبة لنظام التشغيل iOS ، كما يتم جمع الموارد بشكل مختلف وليس كل الموارد في إصدار android ستكون في نظام التشغيل iOS والعكس ؛
  • موقع الإطلاق: من أين نطلق بالضبط - على وكيل الإنشاء حيث تكون جميع الاختبارات المتاحة مطلوبة أو على كمبيوتر المستخدم حيث تحتاج قائمة الشركات الناشئة إلى الحد الأدنى.


نظام اختبار ثابت


يتم تنفيذ جوهر النظام والمجموعة الأساسية من الاختبارات الثابتة في الثعبان. الأساس هو فقط عدد قليل من الكيانات:


اختبار السياق هو مفهوم واسع النطاق. إنه يخزن معلمات الإنشاء والإطلاق ، التي تحدثنا عنها أعلاه ، بالإضافة إلى المعلومات الوصفية التي تملأها الاختبارات وتستخدمها.

تحتاج أولاً إلى فهم الاختبارات التي يجب إجراؤها. لهذا السبب ، تحتوي معلومات التعريف على أنواع الموارد التي نهتم بها على وجه التحديد في هذا الإطلاق. يتم تحديد أنواع الموارد من خلال الاختبارات المسجلة في النظام. يمكن أن يكون "الاختبار" مرتبطًا بنوع واحد أو عدة أنواع ، وإذا وجد في وقت الالتزام أن الملفات التي يفحصها هذا الاختبار قد تغيرت ، فقد تغير المورد المرتبط. يتلاءم هذا الأمر بسهولة مع أيديولوجيتنا - للتشغيل محلياً بأقل عدد ممكن من الفحوصات: إذا لم تتغير الملفات التي يكون الاختبار مسؤولاً عنها ، فلن تحتاج إلى تشغيلها.

على سبيل المثال ، يوجد وصف للسمك ، يشار فيه إلى النموذج والملمس ثلاثي الأبعاد ، وإذا تغير ملف الوصف ، فسيتم التحقق من وجود النموذج والملمس الموضحين فيه. في حالات أخرى ، ليست هناك حاجة لتشغيل فحص الأسماك.

من ناحية أخرى ، قد يتطلب تغيير أحد الموارد تغييرات وكيانات بناءً عليه: إذا تغيرت مجموعة المواد المخزنة في ملفات xml الخاصة بنا ، فمن الضروري التحقق من النماذج ثلاثية الأبعاد الإضافية ، حيث قد يتضح أنه تم حذف النسيج الذي يحتاجه النموذج. يتم تطبيق التحسينات الموضحة أعلاه محليًا على جهاز المستخدم فقط في وقت الالتزام ، وعندما يتم إطلاقها في مزرعة التجميع ، من المفترض أن جميع الملفات قد تغيرت ونجري جميع الاختبارات.

المشكلة التالية هي اعتماد بعض الاختبارات على اختبارات أخرى: من المستحيل التحقق من الأسماك قبل العثور على جميع القوام والنماذج. لذلك ، قسمنا جميع عمليات التنفيذ إلى مرحلتين:

  • إعداد السياق
  • أداء عمليات التفتيش

في المرحلة الأولى ، يتم ملء السياق بمعلومات حول الموارد الموجودة (في حالة الأسماك ، مع محددات الأنماط والقوام). في المرحلة الثانية ، باستخدام المعلومات المخزنة ، تحقق ببساطة من وجود المورد المرغوب. ويرد سياق مبسط أدناه.

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 pip للمصممين والمبرمجين أيضًا أن يمثلوا مشكلة.
  2. هناك برنامج نصي يقوم بتثبيت جميع الحزم اللازمة. هذا أفضل بالفعل ، لكن إذا كان لدى المستخدم تثبيت بيثون خاطئ ، فقد تحدث تعارضات في العمل.
  3. تقديم النسخة الصحيحة من المترجم والتبعيات من تخزين القطع الأثرية (Nexus) وإجراء الاختبارات في بيئة افتراضية.

قضية أخرى هي الأداء. لمزيد من الاختبارات ، كلما تم التحقق من التغيير على جهاز الكمبيوتر الخاص بالمستخدم. كل بضعة أشهر هناك لمحة وتحسين الاختناقات. لذلك تم تحسين السياق ، وظهرت ذاكرة التخزين المؤقت للملفات النصية ، وتم تحسين آليات التقييم (تحديد أن هذا الملف مثير للاهتمام للاختبار).

ثم يبقى فقط حل مشكلة كيفية تطبيق النظام على جميع المشاريع وإجبار جميع المطورين على تضمين خطافات مملوكة مسبقًا ، ولكن هذه قصة مختلفة تمامًا ...

استنتاج


أثناء عملية التطوير ، رقصنا على أشعل النار ، قاتلنا بقوة ، ولكن لا يزال لدينا نظام يسمح لنا بالعثور على أخطاء أثناء الالتزام ، وخفض عمل المختبرين ، وكانت المهام قبل الإصدار حول النسيج المفقود شيء من الماضي. لتحقيق السعادة التامة ، لا يكفي الإعداد البسيط للبيئة وتحسين الاختبارات الفردية ، لكن golems من قسم ci يعملون بجد على هذا.

يمكن العثور على مثال كامل للرمز المستخدم كأمثلة في المقالة في مستودعنا .

Source: https://habr.com/ru/post/ar452926/


All Articles