نفتح اليوم الكود المصدري لـ testuite ، وهو إطار عمل لاختبار خدمات HTTP ، والذي تم تطويره واستخدامه بواسطة Yandex.Taxi. يتم نشر المصادر على GitHub بموجب ترخيص MIT.من خلال testuite ، من السهل اختبار خدمات HTTP. يوفر آليات جاهزة من أجل:تتكون الواجهة الخلفية لـ Yandex.Taxi من مئات الخدمات الدقيقة ، والتي تظهر باستمرار خدمات جديدة. نقوم بتطوير جميع الخدمات عالية التحميل في C ++ باستخدام إطار خادمنا الخاص بنا ، وقد تحدثنا بالفعل عن ذلك في حبري . نصنع خدمات ونماذج أولية أقل تطلبًا في Python.للتأكد من أن الخدمة تحل مشكلتها جيدًا ، وتوفر واجهة برمجة التطبيقات للخدمات الأخرى والتطبيق النهائي ، نريد اختبارها ككل ، بشكل أساسي على أساس الصندوق الأسود.لا توجد أدوات جاهزة لذلك - يجب عليك كتابة التعليمات البرمجية لإعداد بيئة اختبار ، والتي ستكون:- رفع وملء قاعدة البيانات ؛
- اعتراض وانتحال طلبات HTTP ؛
- تشغيل خدمة اختبار في هذه البيئة.
إن حل هذه المشكلة باستخدام أطر عمل اختبارات الوحدة أمر صعب وخاطئ للغاية ، لأن مهمتها مختلفة: اختبار الوحدة للوحدات الهيكلية الأصغر - المكونات والفئات والوظائف.يعتمد Testsuite على pytest ، إطار اختبار Python القياسي. لا يهم اللغة التي كُتبت بها الخدمة الدقيقة التي نختبرها. يعمل موقع testuite الآن على أنظمة تشغيل GNU / Linux و macOS.على الرغم من أن testuite مناسب لسيناريوهات التكامل ، أي تفاعل العديد من الخدمات (وإذا كانت الخدمة مكتوبة في Python ، ثم للخدمة منخفضة المستوى) ، فلن نعتبر هذه الحالات. علاوة على ذلك ، سنركز فقط على اختبار خدمة واحدة.مبدأ التشغيل
الهدف النهائي هو التأكد من أن الخدمة ترد على مكالمات 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)
الاختبارات
تحضير البنية التحتية للاختبارات لبدء الخدمة. نشير إلى الإعدادات التي نريدها لبدء الخدمة.مصدر
@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_info.base_url + 'storage/',
],
check_url=SERVICE_BASEURL + 'ping',
),
) as scope:
yield scope
قمنا بتعيين أداة العميل ، من خلالها يرسل الاختبار طلب HTTP إلى الخدمة.مصدر@pytest.fixture
async def server_client(
service_daemon,
service_client_options,
ensure_daemon_started,
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.handler('/storage/messages/send')
async def handle_send(request):
assert request.json == {
'username': 'Bob',
'text': 'Hello, my name is Bob!',
}
return mockserver.make_response(status=204)
response = await server_client.post(
'messages/send',
json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},
)
assert response.status == 204
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.json_handler('/storage/messages/retrieve')
async def handle_retrieve(request):
return {'messages': messages}
response = await server_client.post('messages/retrieve')
assert response.status == 200
body = response.json()
assert body == {'messages': list(reversed(messages))}
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):
response = await server_client.post(
'/messages/send', json={'username': 'foo', 'text': 'bar'},
)
assert response.status_code == 200
data = response.json()
assert 'id' in data
cursor = pgsql['chat_messages'].cursor()
cursor.execute(
'SELECT username, text FROM messages WHERE id = %s', (data['id'],),
)
record = cursor.fetchone()
assert record == ('foo', 'bar')
لنكتب اختبارًا للطريقة POST messages/retrieve
. تحقق من أنه يقوم بإرجاع الرسائل من قاعدة البيانات.أولاً ، قم بإنشاء برنامج نصي يضيف السجلات التي نحتاجها إلى الجدول. سيقوم Testsuite تلقائيًا بتنفيذ البرنامج النصي قبل الاختبار.مصدر
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');
مصدر
async def test_messages_retrieve(server_client, pgsql):
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
docs/examples$ make run-chat-mongo
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 - اترك تعليقًا.