طرق اختبار `test::nginx`: التكوين، إرسال الطلبات، والتعامل مع الردود
API7.ai
November 18, 2022
بحلول المقال الأخير، كنا قد ألقينا نظرة أولى على test::nginx
وقمنا بتشغيل أبسط مثال. ومع ذلك، في مشروع مفتوح المصدر حقيقي، تكون حالات الاختبار المكتوبة باستخدام test::nginx
أكثر تعقيدًا وصعوبة في الإتقان من نموذج الكود. وإلا، لما كانت تُسمى عقبة.
في هذا المقال، سأأخذك في جولة عبر الأوامر وطرق الاختبار المستخدمة بشكل متكرر في test::nginx
، حتى تتمكن من فهم معظم مجموعات حالات الاختبار في مشروع OpenResty وتكون لديك القدرة على كتابة حالات اختبار أكثر واقعية. حتى إذا لم تكن قد ساهمت بكتابة كود في OpenResty بعد، فإن التعرف على إطار اختبار OpenResty سيكون مصدر إلهام كبير لك لتصميم وكتابة حالات الاختبار في عملك.
اختبار test::nginx
يقوم بشكل أساسي بإنشاء nginx.conf
وبدء عملية NGINX بناءً على تكوين كل حالة اختبار. ثم يقوم بمحاكاة طلب عميل مع جسم الطلب والرؤوس المحددة. بعد ذلك، يقوم كود Lua في حالة الاختبار بمعالجة الطلب وإجراء استجابة. في هذه المرحلة، يقوم test::nginx
بتحليل المعلومات الحرجة مثل جسم الاستجابة ورؤوس الاستجابة وسجلات الأخطاء ومقارنتها بتكوين الاختبار. إذا كان هناك اختلاف، يفشل الاختبار مع خطأ؛ وإلا، يكون ناجحًا.
يوفر test::nginx
الكثير من البدائيات الخاصة بـ DSL (لغة خاصة بالمجال). لقد قمت بتصنيف بسيط وفقًا لتكوين NGINX، وإرسال الطلبات، ومعالجة الاستجابات، وفحص السجلات. هذه الـ 20% من الوظائف يمكن أن تغطي 80% من سيناريوهات التطبيق، لذا يجب أن نتمكن منها بشكل جيد. أما بالنسبة للبدائيات الأخرى الأكثر تقدمًا واستخداماتها، فسنقدمها في المقال القادم.
تكوين NGINX
لننظر أولاً إلى تكوين NGINX. البدائية في test::nginx
التي تحتوي على الكلمة المفتاحية "config" مرتبطة بتكوين NGINX، مثل config
، stream_config
، http_config
، إلخ.
وظائفها متشابهة: إدخال تكوين NGINX المحدد في سياقات NGINX المختلفة. يمكن أن تكون هذه التكوينات إما أوامر NGINX أو كود Lua مغلف في content_by_lua_block
.
عند إجراء اختبارات الوحدة، تكون config
هي البدائية الأكثر استخدامًا حيث نقوم بتحميل مكتبات Lua واستدعاء وظائف لاختبار الصندوق الأبيض. هنا مقتطف من كود الاختبار، والذي لا يمكن تشغيله بالكامل. وهو مأخوذ من مشروع مفتوح المصدر حقيقي، لذا إذا كنت مهتمًا، يمكنك النقر على الرابط لرؤية الاختبار الكامل، أو يمكنك محاولة تشغيله محليًا.
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'})
if not ok then
ngx.say(err)
end
ngx.say("done")
}
}
الغرض من حالة الاختبار هذه هو اختبار ما إذا كانت وظيفة check_schema
في ملف الكود plugins.key-auth
تعمل بشكل صحيح. تستخدم أمر NGINX content_by_lua_block
في location /t
لطلب الوحدة المراد اختبارها واستدعاء الوظيفة التي تحتاج إلى التحقق مباشرة.
هذه وسيلة شائعة لاختبار الصندوق الأبيض في test::nginx
. ومع ذلك، هذا التكوين وحده ليس كافيًا لإكمال الاختبار، لذا دعنا ننتقل ونرى كيفية إرسال طلب عميل.
إرسال الطلبات
محاكاة عميل يرسل طلبًا تتضمن الكثير من التفاصيل، لذا لنبدأ بأبسطها - إرسال طلب واحد.
request
استمرارًا لحالة الاختبار أعلاه، إذا أردنا تشغيل كود اختبار الوحدة، فيجب علينا بدء طلب HTTP إلى العنوان /t
المحدد في التكوين، كما هو موضح في كود الاختبار التالي:
--- request
GET /t
يرسل هذا الكود طلب GET
إلى /t
في بدائية الطلب. هنا، لم نحدد عنوان IP أو اسم النطاق أو المنفذ للوصول، ولم نحدد ما إذا كان HTTP 1.0
أو HTTP 1.1
. كل هذه التفاصيل مخفية بواسطة test::nginx
، لذا لا داعي للاهتمام بها. هذه إحدى فوائد DSL - نحتاج فقط إلى التركيز على منطق الأعمال دون تشتيت الانتباه بكل التفاصيل.
أيضًا، يوفر هذا مرونة جزئية. على سبيل المثال، الإعداد الافتراضي هو بروتوكول HTTP 1.1
، أو إذا أردنا اختبار HTTP 1.0
، يمكننا التحديد بشكل منفصل:
--- request
GET /t HTTP/1.0
بالإضافة إلى طريقة GET
، يجب أيضًا دعم طريقة POST
. في المثال التالي، يمكننا POST
السلسلة hello world
إلى العنوان المحدد.
--- request
POST /t
hello world
مرة أخرى، يقوم test::nginx
بحساب طول جسم الطلب لك هنا، ويضيف رؤوس الطلب host
وconnection
لضمان أن هذا طلب عادي تلقائيًا.
بالطبع، يمكننا إضافة تعليقات لجعلها أكثر قابلية للقراءة. تلك التي تبدأ بـ #
سيتم التعرف عليها كتعليقات كود.
--- request
# post request
POST /t
hello world
يدعم الطلب أيضًا وضعًا أكثر تعقيدًا ومرونة، والذي يستخدم eval
كمرشح لتضمين كود Perl مباشرةً حيث أن test::nginx
مكتوب بلغة Perl. إذا كانت لغة DSL الحالية لا تلبي احتياجاتك، فإن eval
هو "السلاح النهائي" لتنفيذ كود Perl مباشرةً.
بالنسبة لاستخدام eval
، دعنا ننظر إلى بعض الأمثلة البسيطة هنا، وسنستمر في أمثلة أخرى أكثر تعقيدًا في المقال القادم.
--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"
في المثال الأول، نستخدم eval
لتحديد أحرف غير قابلة للطباعة، وهي إحدى استخداماته. المحتوى بين علامتي التنصيص سيتم التعامل معه كسلسلة Perl ثم يتم تمريره إلى request
كوسيطة.
هنا مثال أكثر إثارة للاهتمام:
--- request eval
"POST /t\n" . "a" x 1024
ومع ذلك، لفهم هذا المثال، نحتاج إلى معرفة شيء ما عن السلاسل في Perl، لذا أحتاج إلى ذكر نقطتين هنا باختصار.
- في Perl، نستخدم نقطة لتمثيل دمج السلاسل. أليس هذا مشابهًا إلى حد ما لنقطتي
Lua
؟ - يشير الحرف الصغير
x
إلى عدد مرات تكرار الحرف. على سبيل المثال،"a" x 1024
أعلاه تعني أن الحرف "a" يتكرر 1024 مرة.
لذا، المثال الثاني يعني أن طريقة POST
ترسل طلبًا يحتوي على 1024
حرف a
إلى العنوان /t
.
pipelined_requests
بعد فهم كيفية إرسال طلب واحد، دعنا ننظر إلى كيفية إرسال طلبات متعددة. في test::nginx
، يمكننا استخدام البدائية pipelined_requests
لإرسال طلبات متعددة بالتسلسل ضمن نفس اتصال keep-alive
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
على سبيل المثال، سيقوم هذا المثال بالوصول إلى هذه الواجهات الأربع بالتسلسل في نفس الاتصال. هناك ميزتان حول هذا:
- الأولى هي أنه يمكن التخلص من الكثير من كود الاختبار المتكرر، ويمكن ضغط حالات الاختبار الأربع في واحدة.
- الثانية والأهم هي أنه يمكننا استخدام الطلبات المتسلسلة للكشف عما إذا كان منطق الكود سيكون لديه استثناءات في حالة الوصول المتعدد.
قد تتساءل، أكتب حالات اختبار متعددة بالتسلسل، ثم سيتم تنفيذ الكود أيضًا عدة مرات في مرحلة التنفيذ. أليس هذا يغطي أيضًا المشكلة الثانية أعلاه؟
يتعلق الأمر بطريقة تنفيذ test::nginx
، والتي تعمل بشكل مختلف عما قد تعتقده. بعد كل حالة اختبار، يقوم test::nginx
بإيقاف عملية NGINX الحالية، وتختفي جميع البيانات في الذاكرة. عند تشغيل حالة الاختبار التالية، يتم إنشاء nginx.conf
من جديد، ويتم بدء Worker
NGINX جديد. هذه الآلية تضمن أن حالات الاختبار لا تؤثر على بعضها البعض.
لذا، عندما نريد اختبار طلبات متعددة، نحتاج إلى استخدام البدائية pipelined_requests
. بناءً عليها، يمكننا محاكاة تحديد المعدل، وتحديد التزامن، والعديد من السيناريوهات الأخرى لاختبار ما إذا كان نظامك يعمل بشكل صحيح مع سيناريوهات أكثر واقعية وتعقيدًا. سنترك هذا أيضًا للمقال القادم، حيث سيتم تضمين أوامر وبدائيات متعددة.
repeat_each
لقد ذكرنا للتو حالة اختبار طلبات متعددة، فكيف يجب أن ننفذ نفس الاختبار عدة مرات؟
بالنسبة لهذه المشكلة، يوفر test::nginx
إعدادًا عامًا: repeat_each
، وهي وظيفة Perl تكون افتراضيًا repeat_each(1)
، مما يشير إلى أن حالة الاختبار ستتم مرة واحدة فقط. لذا في حالات الاختبار السابقة، لا نزعج أنفسنا بتعيينها بشكل منفصل.
بطبيعة الحال، يمكننا تعيينها قبل وظيفة run_test()
، على سبيل المثال، بتغيير الوسيطة إلى 2
.
repeat_each(2);
run_tests();
ثم، يتم تشغيل كل حالة اختبار مرتين، وهكذا.
more_headers
بعد الحديث عن جسم الطلب، دعنا ننظر إلى رؤوس الطلب. كما ذكرنا أعلاه، يقوم test::nginx
بإرسال الطلب مع رؤوس host
وconnection
بشكل افتراضي. ماذا عن رؤوس الطلب الأخرى؟
تم تصميم more_headers
خصيصًا للقيام بذلك.
--- more_headers
X-Foo: blah
يمكننا استخدامه لتعيين رؤوس مخصصة متنوعة. إذا أردنا تعيين أكثر من رأس واحد، فقم بتعيين أكثر من سطر:
--- more_headers
X-Foo: 3
User-Agent: openresty
معالجة الاستجابات
بعد إرسال الطلب، الجزء الأكثر أهمية في test::nginx
هو معالجة الاستجابة، حيث سنحدد ما إذا كانت الاستجابة تفي بالتوقعات. هنا نقسمها إلى أربعة أجزاء ونقدمها: جسم الاستجابة، رأس الاستجابة، رمز حالة الاستجابة، والسجل.
response_body
نظير بدائية الطلب هو response_body
، وفيما يلي مثال على استخدامهما معًا:
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
ngx.say("hello")
}
}
--- request
GET /t
--- response_body
hello
ستنجح حالة الاختبار هذه إذا كان جسم الاستجابة هو hello
، وستبلغ عن خطأ في حالات أخرى. ولكن كيف نختبر جسم استجابة طويل؟ لا تقلق، test::nginx
قد قام بذلك نيابةً عنك. يدعم الكشف عن جسم الاستجابة باستخدام تعبير عادي، مثل التالي:
--- response_body_like
^he\w+$
هذا يسمح لك بالمرونة الكبيرة مع جسم الاستجابة. بالإضافة إلى ذلك، يدعم test::nginx
أيضًا عمليات unlike
:
--- response_body_unlike
^he\w+$
في هذه المرحلة، إذا كان جسم الاستجابة هو hello
، فلن ينجح الاختبار.
على نفس المنوال، بعد فهم الكشف عن طلب واحد، دعنا ننظر إلى الكشف عن طلبات متعددة. هنا مثال على كيفية استخدامه مع pipelined_requests
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]
بالطبع، الشيء المهم الذي يجب ملاحظته هنا هو أنه بقدر ما ترسل من طلبات، تحتاج إلى وجود استجابات مقابلة لها.
response_headers
ثانيًا، دعنا نتحدث عن رأس الاستجابة. رأس الاستجابة مشابه لرأس الطلب حيث أن كل سطر يقابل مفتاح وقيمة رأس.
--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1
مثل الكشف عن جسم الاستجابة، تدعم رؤوس الاستجابة أيضًا التعبيرات العادية وعمليات unlike
، مثل response_headers_like
، raw_response_headers_like
، وraw_response_headers_unlike
.
error_code
الثالث هو رمز الاستجابة. يدعم الكشف عن رمز الاستجابة المقارنة المباشرة ويدعم أيضًا عمليات like
، مثل المثالين التاليين:
--- error_code: 302
--- error_code_like: ^(?:500)?$
في حالة الطلبات المتعددة، يجب التحقق من error_code
عدة مرات:
--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]
error_log
عنصر الاختبار الأخير هو سجل الأخطاء. في معظم حالات الاختبار، لا يتم إنشاء سجل أخطاء. يمكننا استخدام no_error_log
للكشف:
--- no_error_log
[error]
في المثال أعلاه، إذا ظهرت السلسلة [error]
في error.log
الخاص بـ NGINX، فسيفشل الاختبار. هذه ميزة شائعة جدًا، ويوصى بإضافة الكشف عن سجل الأخطاء إلى جميع اختباراتك العادية.
--- error_log
hello world
التكوين أعلاه يكشف عن وجود hello world
في error.log
. بالطبع، يمكنك استخدام eval
المضمن في كود Perl لتنفيذ الكشف باستخدام التعبيرات العادية، مثل التالي:
--- error_log eval
qr/\[notice\] .*? \d+ hello world/
الخلاصة
اليوم، نتعلم كيفية إرسال الطلبات واختبار الاستجابات في test::nginx
، بما في ذلك جسم الطلب، الرأس، رمز حالة الاستجابة، وسجل الأخطاء. يمكننا تنفيذ مجموعة كاملة من حالات الاختبار باستخدام مجموعة هذه البدائيات.
أخيرًا، هنا سؤال للتفكير: ما هي مزايا وعيوب test::nginx
، وهي لغة DSL مجردة؟ لا تتردد في ترك تعليقات والمناقشة معي، وأنت أيضًا مرحب بك لمشاركة هذه المقالة للتواصل والتفكير معًا.