طرق اختبار `test::nginx`: التكوين، إرسال الطلبات، والتعامل مع الردود

API7.ai

November 18, 2022

OpenResty (NGINX + Lua)

بحلول المقال الأخير، كنا قد ألقينا نظرة أولى على 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 مجردة؟ لا تتردد في ترك تعليقات والمناقشة معي، وأنت أيضًا مرحب بك لمشاركة هذه المقالة للتواصل والتفكير معًا.