مكتبة وهمية أخرى

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


تطبيق


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


لذا ، تخيل أن لدينا نظامًا يحتوي على المكونات التالية:


  1. خادم التكوين. بالنسبة للمستخدم ، هذه نقطة إدخال واحدة حيث يقوم بتكوين ليس فقط تطبيق التحقق ، ولكن أيضًا مكونات النظام الأخرى.
  2. تطبيق التحقق.
  3. البيانات من تطبيقات معالجة التطبيقات التي يتم تخزينها في elasticsearch.
  4. البيانات المرجعية. يعتمد تنسيق البيانات على برنامج المراسلة الذي تم دمج التطبيق معه.

مهمة


يبدو اختبار الأتمتة في هذه الحالة واضحًا تمامًا:


  1. إعداد البيئة:
    • يتم تثبيت Elasticsearch مع الحد الأدنى من التكوين (باستخدام msi وسطر الأوامر).
    • تم تثبيت تطبيق التحقق.
  2. تنفيذ الاختبار:
    • تم تكوين تطبيق التحقق.
    • تمتلئ Elasticsearch ببيانات الاختبار للاختبار المقابل (عدد التطبيقات التي تمت معالجتها).
    • يتلقى التطبيق بيانات "مرجعية" من برنامج المراسلة (عدد التطبيقات التي كان من المفترض أنها في الواقع).
    • يتم التحقق من الحكم الصادر من التطبيق: عدد الطلبات التي تم التحقق منها بنجاح ، وعدد الطلبات المفقودة ، إلخ.
  3. تنظيف البيئة.

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


قد يثير هذا السؤال: "إذا كنا لا نزال نستهزئ بالخادم ، فربما لا يمكننا قضاء بعض الوقت في تثبيت وتعبئة elasticsearch ، ولكن بدلاً من الاستهزاء؟". ولكن لا يزال ، عليك دائمًا أن تتذكر أن استخدام النموذج يوفر مرونة ، لكنه يضيف التزامًا بمراقبة أهمية سلوك النموذج. لذلك ، رفضت استبدال elasticsearch: إنه من السهل جدًا تثبيته وتعبئته.


أول وهمية


يرسل الخادم التكوين إلى طلبات GET بعدة طرق في / التكوين. نحن مهتمون بطريقتين. الأول هو /configuration/data_cluster مع تكوين الكتلة


 { "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass" } } 

والثاني هو /configuration/reconciliation مع تكوين تطبيق الحفر


 { "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass" } } } 

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


لذلك ، لن تعمل لنا أدوات التهوية الثابتة وأدوات mocks في اختبارات الوحدة (وهمية ، monkeypatch من pytest ، وما إلى ذلك). لقد وجدت مكتبة pretenders رائعة اعتقدت أنها مناسبة لي. يوفر Pretenders القدرة على إنشاء خادم HTTP بقواعد تحدد كيفية استجابة الخادم للطلبات. يتم تخزين القواعد في إعدادات مسبقة ، مما يسمح بعزل الأحجام عن مجموعات اختبار مختلفة. يمكن مسح الإعدادات المسبقة وإعادة تعبئتها ، مما يسمح لك بتحديث الإجابات حسب الحاجة. يكفي رفع الخادم نفسه مرة واحدة أثناء إعداد البيئة:


 python -m pretenders.server.server --host 127.0.0.1 --port 8000 

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


 import json import pytest from pretenders.client.http import HTTPMock from pretenders.common.constants import FOREVER @pytest.fixture def configuration_server_mock(request): mock = HTTPMock(host="127.0.0.1", port=8000, name="server") request.addfinalizer(mock.reset) return mock def test_something(configuration_server_mock): configuration_server_mock.when("GET /configuration/data_cluster").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, }), status=200, times=FOREVER, ) configuration_server_mock.when("GET /configuration/reconciliation").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass", }, }, }), status=200, times=FOREVER, ) # test application 

لكن هذا ليس كل شيء. بفضل مرونتها ، يوجد لدى pretenders يجب تذكرهما ويجب معالجتهما في حالتنا:


  1. لا يمكن حذف القواعد واحدة تلو الأخرى. لتغيير الإجابة ، يجب عليك حذف الإعداد المسبق بأكمله وإعادة إنشاء جميع القواعد مرة أخرى.
  2. جميع المسارات المستخدمة في القواعد هي نسبية. الإعدادات المسبقة لها مسار فريد للنموذج / mockhttp / <preset_name> ، وهذا المسار هو بادئة شائعة لجميع المسارات التي تم إنشاؤها في القواعد. يستقبل التطبيق قيد الاختبار اسم المضيف فقط ، ولا يمكنه معرفة البادئة.

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


 configuration.data_cluster.port = 443 

أو (لجعل طلبات التحديث أقل تواترا)


 data_cluster_config = get_default_data_cluster_config() data_cluster_config.port = 443 configuration.update_data_cluster(data_cluster_config) 

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


للتخلص من البادئات ، يمكنك استخدام مكتبة mitmproxy . هذه أداة قوية تتيح ، من بين أشياء أخرى ، إعادة توجيه الطلبات. سنقوم بإزالة البادئات كما يلي:


 mitmdump --mode reverse:http://127.0.0.1:8000 --replacements :~http:^/:/mockhttp/server/ --listen-host 127.0.01 --listen-port 80 

تقوم معلمات هذا الأمر بما يلي:


  1. --listen-host 127.0.0.1 و --listen-port 80 . يقوم Mitmproxy برفع الخادم الخاص به ، وبهذه المعلمات نحدد الواجهة والمنفذ اللذين سيستمع إليه هذا الخادم.
  2. --mode reverse:http://127.0.0.1:8000 يعني أنه سيتم إعادة توجيه الطلبات إلى خادم mitproxy إلى http://127.0.0.1:8000 . اقرأ المزيد هنا .
  3. --replacements :~http:^/:/mockhttp/server/ يحدد القالب الذي سيتم تغيير الطلبات. يتكون من ثلاثة أجزاء: عامل تصفية طلب ( ~http لطلبات HTTP) ، قالب للتغيير ( ^/ لاستبدال بداية المسار) ، واستبدال فعليًا ( /mockhttp/server ). اقرأ المزيد هنا .

في حالتنا ، نضيف mockhttp/server إلى جميع طلبات HTTP mockhttp/server توجيهها إلى http://127.0.0.1:8000 ، أي إلى أدعياء الخادم الخاص بنا. نتيجة لذلك ، حققنا أنه يمكن الآن الحصول على التهيئة بطلب الحصول على GET إلى http://127.0.0.1/configuration/data_cluster .


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


وهمية الثانية


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


 GET /application-history?page=2&size=5&start=1569148012&end=1569148446 

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


حاولت البحث عن حلول بديلة ، لكن في النهاية قررت أن أحاول كتابة حلواني. لذلك كان هناك خادم فضفاض . هذا هو تطبيق Flask حيث يمكن تكوين المسارات والاستجابات بعد أن يبدأ. من خارج الصندوق ، يعرف كيفية التعامل مع قواعد لنوع الطلب (GET ، POST ، وما إلى ذلك) وللمسار. هذا يتيح لك استبدال pretenders و mitmproxy في المهمة الأصلية. سأوضح أيضًا كيف يمكن استخدامه لإنشاء نموذج لواجهة برمجة تطبيقات البائع.


يحتاج التطبيق إلى مسارين رئيسيين:


  1. نقطة النهاية قاعدة. هذه هي نفس البادئة التي سيتم استخدامها لجميع القواعد المكونة.
  2. نقطة نهاية التكوين. هذا هو بادئة تلك الطلبات التي يمكنك من خلالها تكوين خادم الصور نفسه.

 python -m looseserver.default.server.run --host 127.0.0.1 --port 80 --base-endpoint / --configuration-endpoint /_mock_configuration/ 

بشكل عام ، من الأفضل عدم تكوين نقطة النهاية الأساسية ونقطة النهاية التكوين بحيث يكون أحدهما هو الأصل بالنسبة للآخر. خلاف ذلك ، هناك احتمال أن تتعارض مسارات التكوين والاختبار. سيكون لنقطة نهاية التكوين الأسبقية ، حيث تتم إضافة قواعد Flask للتكوين في وقت مبكر عن المسارات الديناميكية. في حالتنا ، يمكننا استخدام --base-endpoint /configuration/ إذا لم نقم بإدراج واجهة برمجة تطبيقات البائع في هذا النموذج.


أبسط نسخة من الاختبارات لا تتغير كثيرا


 import json import pytest from looseserver.default.client.http import HTTPClient from looseserver.default.client.rule import PathRule from looseserver.default.client.response import FixedResponse @pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = HTTPClient(configuration_url="http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_rule(self, path, json_response): rule = self._client.create_rule(PathRule(path=path)) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) def _delete_rules(self): for rule_id in self._rule_ids: self._client.remove_rule(rule_id=rule_id) mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): configuration_server_mock.create_rule( path="configuration/data_cluster", json_response={ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, } ) configuration_server_mock.create_rule( path="configuration/reconciliation", json_response={ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///applications", "credentials": { "username": "user", "password": "pass", }, }, } ) 

أصبحت ميزة الثابت أكثر صعوبة ، ولكن يمكن الآن حذف القواعد واحدة تلو الأخرى ، مما يبسط العمل معهم. باستخدام mitmproxy لم تعد هناك حاجة.


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


يجب إنشاء قواعد وإجابات جديدة لكل من الخادم والعميل. لنبدأ مع الخادم:


 from looseserver.server.rule import ServerRule class ServerParameterRule(ServerRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER"): super(ServerParameterRule, self).__init__(rule_type=rule_type) self._parameter_name = parameter_name self._parameter_value = parameter_value def is_match_found(self, request): if self._parameter_value is None: return self._parameter_name not in request.args return request.args.get(self._parameter_name) == self._parameter_value 

يجب أن تحدد كل قاعدة طريقة is_match_found ، والتي تحدد ما إذا كان يجب أن تعمل لطلب معين أم لا. معلمة الإدخال الخاصة به هي كائن الطلب. بعد إنشاء القاعدة الجديدة ، من الضروري "تعليم" الخادم لقبوله من العميل. للقيام بذلك ، استخدم RuleFactory :


 from looseserver.default.server.rule import create_rule_factory from looseserver.default.server.application import configure_application def _create_application(base_endpoint, configuration_endpoint): server_rule_factory = create_rule_factory(base_endpoint) def _parse_param_rule(rule_type, parameters): return ServerParameterRule( rule_type=rule_type, parameter_name=parameters["parameter_name"], parameter_value=parameters["parameter_value"], ) server_rule_factory.register_rule( rule_type="PARAMETER", parser=_parse_param_rule, serializer=lambda rule_type, rule: None, ) return configure_application( rule_factory=server_rule_factory, base_endpoint=base_endpoint, configuration_endpoint=configuration_endpoint, ) if __name__ == "__main__": application = _create_application(base_endpoint="/", configuration_endpoint="/_mock_configuration") application.run(host="127.0.0.1", port=80) 

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


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


 from looseserver.client.rule import ClientRule from looseserver.default.client.rule import create_rule_factory class ClientParameterRule(ClientRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER", rule_id=None): super(ClientParameterRule, self).__init__(rule_type=rule_type, rule_id=rule_id) self.parameter_name = parameter_name self.parameter_value = parameter_value def _create_client(configuration_url): def _serialize_param_rule(rule): return { "parameter_name": rule.parameter_name, "parameter_value": rule.parameter_value, } client_rule_factory = create_rule_factory() client_rule_factory.register_rule( rule_type="PARAMETER", parser=lambda rule_type, parameters: ClientParameterRule(rule_type=rule_type, parameter_name=None), serializer=_serialize_param_rule, ) return HTTPClient(configuration_url=configuration_url, rule_factory=client_rule_factory) 

يبقى استخدام _create_client لإنشاء العميل ، ويمكن استخدام القواعد في الاختبارات. في المثال أدناه ، أضفت استخدام قاعدة افتراضية أخرى: CompositeRule . يسمح لك بدمج العديد من القواعد في قاعدة واحدة بحيث تعمل فقط إذا كانت كل واحدة منها تُرجع "True" للاتصال is_match_found .


 @pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = _create_client("http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_paged_rule(self, path, page, json_response): rule_prototype = CompositeRule( children=[ PathRule(path=path), ClientParameterRule(parameter_name="page", parameter_value=page), ] ) rule = self._client.create_rule(rule_prototype) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) ... mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): ... configuration_server_mock.create_paged_rule( path="application-history", page=None, json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="1", json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="2", json_response=["4", "5"], ) 

استنتاج


يوفر mitmproxy pretenders mitmproxy أداة قوية ومرنة بما يكفي لإنشاء mocks. مزاياه:


  1. سهل الإعداد.
  2. القدرة على عزل مجموعات الاستعلام باستخدام الإعدادات المسبقة.
  3. حذف مجموعة معزولة بالكامل في وقت واحد.

عن طريق سلبيات ما يلي:


  1. الحاجة إلى إنشاء تعبيرات منتظمة للقواعد.
  2. عدم القدرة على تغيير القواعد بشكل فردي.
  3. وجود بادئة لكل المسارات التي تم mitmproxy أو استخدام اعادة التوجيه باستخدام mitmproxy .

روابط الوثائق:
الأدعياء
Mitmproxy
خادم فضفاض

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


All Articles