يفتح Yandex Testsuite



نفتح اليوم الكود المصدري لـ testuite ، وهو إطار عمل لاختبار خدمات HTTP ، والذي تم تطويره واستخدامه بواسطة Yandex.Taxi. يتم نشر المصادر على GitHub بموجب ترخيص MIT.

من خلال testuite ، من السهل اختبار خدمات HTTP. يوفر آليات جاهزة من أجل:

  • HTTP API.
  • HTTP-, .
  • , .
  • , .


تتكون الواجهة الخلفية لـ Yandex.Taxi من مئات الخدمات الدقيقة ، والتي تظهر باستمرار خدمات جديدة. نقوم بتطوير جميع الخدمات عالية التحميل في C ++ باستخدام إطار خادمنا الخاص بنا ، وقد تحدثنا بالفعل عن ذلك في حبري . نصنع خدمات ونماذج أولية أقل تطلبًا في Python.

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

لا توجد أدوات جاهزة لذلك - يجب عليك كتابة التعليمات البرمجية لإعداد بيئة اختبار ، والتي ستكون:

  • رفع وملء قاعدة البيانات ؛
  • اعتراض وانتحال طلبات HTTP ؛
  • تشغيل خدمة اختبار في هذه البيئة.

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

يعتمد Testsuite على pytest ، إطار اختبار Python القياسي. لا يهم اللغة التي كُتبت بها الخدمة الدقيقة التي نختبرها. يعمل موقع testuite الآن على أنظمة تشغيل GNU / Linux و macOS.

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

مستوى التفاصيلأداة الاختبار
المنهج / الوظيفة ، الفصل ، المكون ، المكتبةاختبارات الوحدة القياسية ، pytest ، Googletest ، لا تزال في بعض الأحيان اختبارية
خدمة متناهية الصغرحزمة اختبار
فرقة Microservice (التطبيق)اختبارات التكامل Testsuite (غير مشمولة في هذه المقالة)

مبدأ التشغيل


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

بدء / إيقاف الخدمة هي عملية روتينية. لذلك ، نتحقق من:

  • أنه بعد بدء الخدمة يستجيب عبر HTTP ؛
  • كيف تتصرف الخدمة إذا كانت الخدمات الخارجية غير متاحة مؤقتًا.




حزمة اختبار:

  • بدء قاعدة البيانات (PostgreSQL، MongoDB ...).
  • قبل كل اختبار ، يملأ قاعدة البيانات ببيانات الاختبار.
  • بدء الخدمة المصغرة المختبرة في عملية منفصلة.
  • تطلق خادم الويب الخاص بها (mockserver) ، الذي يقلد (يجفف) البيئة الخارجية للخدمة.
  • يقوم بإجراء الاختبارات.

يمكن للاختبارات التحقق من:

  • ما إذا كانت الخدمة تتعامل مع طلبات HTTP بشكل صحيح.
  • كيف تعمل الخدمة مباشرة في قاعدة البيانات.
  • وجود / غياب / تسلسل المكالمات إلى الخدمات الخارجية.
  • الحالة الداخلية للخدمة باستخدام المعلومات التي تمر بها إلى Testpoint.

خادع


نختبر سلوك خدمة صغيرة واحدة. يجب تجسير الاستدعاءات إلى HTTP API للخدمات الخارجية. لهذا الجزء من العمل في testuite تلبية المكونات الخاصة بها mockserverو mockserver_https. Mockserver هو خادم HTTP مع معالجات طلبات قابلة للتخصيص لكل اختبار وذاكرة حول الطلبات التي تتم معالجتها والبيانات التي يتم نقلها.

قاعدة البيانات


Testsuite يسمح للاختبار بالوصول مباشرة إلى قاعدة البيانات للقراءة والكتابة. باستخدام البيانات ، يمكنك صياغة شرط مسبق للاختبار والتحقق من النتيجة. من خارج منطقة الجزاء ، يدعم PostgreSQL و MongoDB و Redis.

كيفية البدء باستخدام


لكتابة اختبارات اختبارية ، يجب أن يعرف المطور Python وإطار pytest القياسي .

دعنا نظهر استخدام testuite خطوة بخطوة باستخدام مثال دردشة بسيط. فيما يلي رموز المصدر للتطبيق والاختبارات. يتفاعل chat.html



الواجهة الأمامية مع خدمة الواجهة الخلفية للدردشة . لتوضيح كيفية تفاعل الخدمات ، يقوم مفوضو الواجهة الخلفية للمحادثة بتخزين رسائل التخزين لخدمة المستودع. يتم تنفيذ التخزين بطريقتين ، chat-storage-mongo و chat-storage-postgres .



خلفية الدردشة


خدمة الواجهة الخلفية للدردشة هي نقطة الدخول للطلبات المقدمة من الواجهة الأمامية. قادرة على إرسال و إرجاع قائمة الرسائل.

الخدمات


نعرض معالج طلب مثال POST /messages/retrieve:

رمز المصدر

@routes.post('/messages/retrieve')
async def handle_list(request):
async with aiohttp.ClientSession() as session:
    #     
    response = await session.post(
        storage_service_url + 'messages/retrieve',
            timeout=HTTP_TIMEOUT,
        )
        response.raise_for_status()
        response_body = await response.json()

        #    ,      
        messages = list(reversed(response_body['messages']))
        result = {'messages': messages}
        return web.json_response(result)

الاختبارات


تحضير البنية التحتية للاختبارات لبدء الخدمة. نشير إلى الإعدادات التي نريدها لبدء الخدمة.

مصدر

#      . 
#       ( scope='session'),   
@pytest.fixture(scope='session')
async def service_daemon(
        register_daemon_scope, service_spawner, mockserver_info,
):
    python_path = os.getenv('PYTHON3', 'python3')
    service_path = pathlib.Path(__file__).parent.parent
    async with register_daemon_scope(
            name='chat-backend',
            spawn=service_spawner(
                #   .    —  ,
                #    
                [
                    python_path,
                    str(service_path.joinpath('server.py')),
                    '--storage-service-url',
                    #       mockserver,
                    #         mockserver   /storage
                    mockserver_info.base_url + 'storage/',
                ],
                #  URL,      
                check_url=SERVICE_BASEURL + 'ping',
            ),
    ) as scope:
        yield scope

قمنا بتعيين أداة العميل ، من خلالها يرسل الاختبار طلب HTTP إلى الخدمة.

مصدر

@pytest.fixture
async def server_client(
        service_daemon, # HTTP-  == 204
        service_client_options,
        ensure_daemon_started,
        #   mockserver ,      ,
        #    ,    
        mockserver,
):
    await ensure_daemon_started(service_daemon)
    yield service_client.Client(SERVICE_BASEURL, **service_client_options)

الآن البنية التحتية تعرف كيف تبدأ chat-backendوكيف ترسل طلبًا إليها. هذا يكفي لبدء كتابة الاختبارات.

يرجى ملاحظة أنه في الاختبارات chat-backendلا نستخدم خدمات التخزين chat-storage-mongo، ولا chat-storage-postgres. ل chat-backendيدعو مقبض عادة، ونحن الرطب API التخزين مع mockserver.

لنكتب اختبارًا للطريقة POST messages/send. نحن نتحقق من أن:

  • يتم معالجة الطلب بشكل طبيعي ؛
  • عند معالجة الطلب ، chat-backendيستدعي طريقة التخزين POST messages/send.

مصدر

async def test_messages_send(server_client, mockserver):
    #    mockserver   POST messages/send
    @mockserver.handler('/storage/messages/send')    
    async def handle_send(request):
        # ,       ,
        #     chat-backend
        assert request.json == {
            'username': 'Bob',
            'text': 'Hello, my name is Bob!',
        }
        return mockserver.make_response(status=204)

    #    chat-backend
    response = await server_client.post(
        'messages/send',
        json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},
    )
    
    # ,        HTTP-
    assert response.status == 204

    # ,  chat-backend       POST messages/send
    assert handle_send.times_called == 1

لنكتب اختبارًا للطريقة POST messages/retrieve. نحن نتحقق من أن:

  • يتم معالجة الطلب بشكل طبيعي ؛
  • عند معالجة الطلب ، chat-backendيستدعي طريقة التخزين POST /messages/retrieve؛
  • chat-backend "قلب" قائمة الرسائل المستلمة من المستودع بحيث تكون آخر الرسائل في نهاية القائمة.

مصدر

async def test_messages_retrieve(server_client, mockserver):
    messages = [
        {
            'username': 'Bob',
            'created': '2020-01-01T12:01:00.000',
            'text': 'Hi, my name is Bob!',
        },
        {
            'username': 'Alice',
            'created': {'$date': '2020-01-01T12:02:00.000'},
            'text': 'Hi Bob!',
        },
    ]

    #    mockserver   POST messages/retrieve
    @mockserver.json_handler('/storage/messages/retrieve')
    async def handle_retrieve(request):
        return {'messages': messages}

    #    chat-backend
    response = await server_client.post('messages/retrieve')

    # ,        HTTP-
    assert response.status == 200

    body = response.json()
    
    # ,    chat-backend    ,
    #   ,       
    assert body == {'messages': list(reversed(messages))}

    # ,  chat-backend       POST messages/retrieve
    assert handle_retrieve.times_called == 1


دردشة التخزين postgres



الخدمة chat-storage-postgresمسؤولة عن قراءة وكتابة رسائل الدردشة في قاعدة بيانات PostgreSQL.

الخدمات


هذه هي الطريقة التي نقرأ بها قائمة الرسائل من PostgreSQL في الطريقة POST /messages/retrieve:

Source Code

@routes.post('/messages/retrieve')
    async def get(request):
        async with app['pool'].acquire() as connection:
            records = await connection.fetch(
                'SELECT created, username, "text" FROM messages '
                'ORDER BY created DESC LIMIT 20',
            )
        messages = [
            {
                'created': record[0].isoformat(),
                'username': record[1],
                'text': record[2],
            }
            for record in records
        ]
        return web.json_response({'messages': messages})

الاختبارات


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

مصدر

@pytest.fixture(scope='session')
def pgsql_local(pgsql_local_create):
    # ,     
    tests_dir = pathlib.Path(__file__).parent
    sqldata_path = tests_dir.joinpath('../schemas/postgresql')
    databases = discover.find_databases('chat_storage_postgres', sqldata_path)
    return pgsql_local_create(list(databases.values()))

ما تبقى من إعداد بنية أساسية conftest.py لا يختلف عن الخدمة الموضحة أعلاه chat-backend.

دعنا ننتقل إلى الاختبارات.

لنكتب اختبارًا للطريقة POST messages/send. تحقق من أنه يحفظ الرسالة في قاعدة البيانات.

مصدر

async def test_messages_send(server_client, pgsql):
    #   POST /messages/send
    response = await server_client.post(
        '/messages/send', json={'username': 'foo', 'text': 'bar'},
    )

    # ,    
    assert response.status_code == 200

    # ,     JSON    
    data = response.json()
    assert 'id' in data

    #     PostgreSQL  
    cursor = pgsql['chat_messages'].cursor()
    cursor.execute(
        'SELECT username, text FROM messages WHERE id = %s', (data['id'],),
    )
    record = cursor.fetchone()

    # ,          , 
    #     HTTP-
    assert record == ('foo', 'bar')

لنكتب اختبارًا للطريقة POST messages/retrieve. تحقق من أنه يقوم بإرجاع الرسائل من قاعدة البيانات.

أولاً ، قم بإنشاء برنامج نصي يضيف السجلات التي نحتاجها إلى الجدول. سيقوم Testsuite تلقائيًا بتنفيذ البرنامج النصي قبل الاختبار.

مصدر

--  chat-storage-postgres/tests/static/test_service/pg_chat_messages.sql
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:00.0+03', 'foo', 'hello, world!');
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:01.0+03', 'bar', 'happy ny');

مصدر

#  chat-storage-postgres/tests/test_service.py
async def test_messages_retrieve(server_client, pgsql):
    #     testsuite     
    #  pg_chat_messages.sql
    response = await server_client.post('/messages/retrieve', json={})
    assert response.json() == {
        'messages': [
            {
                'created': '2019-12-31T21:00:01+00:00',
                'text': 'happy ny',
                'username': 'bar',
            },
            {
                'created': '2019-12-31T21:00:00+00:00',
                'text': 'hello, world!',
                'username': 'foo',
            },
        ],
    }

إطلاق


الأمثلة الجارية أسهل في حاوية عامل ميناء. للقيام بذلك ، تحتاج إلى تركيب عامل إرساء ورصيف على الجهاز.

يتم تشغيل جميع الأمثلة من دليل بدء الدردشة .docs/examples



#   MongoDB
docs/examples$ make run-chat-mongo

#   PostgreSQL
docs/examples$ make run-chat-postgres

بعد الإطلاق ، سيتم عرض عنوان URL في وحدة التحكم ، حيث يمكنك فتح الدردشة في المتصفح:

chat-postgres_1 | ======== Running on http://0.0.0.0:8081 ========
chat-postgres_1 | (Press CTRL+C to quit)

إبدأ الاختبارات

#    
docs/examples$ make docker-runtests

#    
docs/examples$ make docker-runtests-mockserver-example
docs/examples$ make docker-runtests-mongo-example
docs/examples$ make docker-runtests-postgres-example

توثيق


وثائق الاختبارات التفصيلية متوفرة هنا .

تعليمات إعداد وتشغيل الأمثلة.

إذا كانت لديك أسئلة github.com/yandex/yandex-taxi-testsuite/issues - اترك تعليقًا.

All Articles