أكدت الراحة: ما تعلمناه من خمس سنوات من استخدام الأداة

REST Assured - DSL لاختبار خدمات REST ، وهو مضمن في اختبارات Java. ظهر هذا الحل منذ أكثر من تسع سنوات وأصبح شائعًا بسبب بساطته ووظائفه المريحة.


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


هذه المقالة تدور حول هذه الميزات الضمنية لـ REST Assured. يجب أخذها في الاعتبار إذا كانت هناك فرصة لزيادة عدد الاختبارات في المشروع بسرعة - بحيث لا تضطر إلى إعادة كتابتها لاحقًا.


صورة


ما الذي نختبره؟


تشارك DINS في تطوير منصة UCaaS. على وجه الخصوص ، نقوم بتطوير واختبار واجهة برمجة التطبيقات التي يستخدمها RingCentral ويوفرها لمطوري الطرف الثالث .


عند تطوير أي واجهة برمجة تطبيقات ، من المهم التأكد من أنها تعمل بشكل صحيح ، ولكن عند إعطائها ، عليك التحقق من الكثير من الحالات. لذلك ، تتم إضافة عشرات ومئات الاختبارات إلى كل نقطة نهاية جديدة. تتم كتابة الاختبارات في Java ، ويتم تحديد TestNG كإطار اختبار ، ويتم استخدام REST Assured لطلبات API.


عندما REST أكد سوف تستفيد


إذا لم يكن هدفك هو اختبار واجهة برمجة التطبيقات بالكامل ، فإن أسهل طريقة للقيام بذلك هي اختبار REST Assured. إنها مناسبة تمامًا لفحص هيكل الاستجابة ، واختبارات PVD ، والدخان.


هذه هي الطريقة التي يظهر بها اختبار بسيط ، والذي سيتحقق من أن نقطة النهاية تعطي الحالة 200 OK عند الوصول إليها:


given() .baseUri("http://cookiemonster.com") .when() .get("/cookies") .then() .assertThat() .statusCode(200); 

تحدد الكلمات الرئيسية given when then تشكل الطلب: given ما سيتم إرساله في الطلب ، when - مع الطريقة التي ونقطة النهاية التي نرسل بها الطلب ، then - كيف يتم فحص الاستجابة المستلمة. بالإضافة إلى ذلك ، يمكنك استخراج نص الاستجابة في شكل كائن من النوع JsonPath أو XmlPath ، ثم لاستخدام البيانات المستلمة.


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


لهذا ، في REST أكد هناك:


  • RequestSpecification / ResponseSpecification ؛
  • التكوين الأساسي.
  • مرشحات.

RequestSpecification و ResponseSpecification


تسمح لك هاتان الفئتان بتحديد معلمات الطلب وتوقعاته من الاستجابة:


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); requestSpec.when() .get("/cookiesformonster") .then() .statusCode(200); requestSpec.when() .get("/soup") .then() .statusCode(400); 

 ResponseSpecification responseSpec = expect() .statusCode(200); given() .expect() .spec(responseSpec) .when() .get("/hello"); given() .expect() .spec(responseSpec) .when() .get("/goodbye"); 

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


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); RequestSpecification yetAnotherRequestSpec = given() .header("Language", "fr"); given() .spec(requestSpec) .spec(yetAnotherRequestSpec) .when() .get("/cookies") .then() .statusCode(200); 

سجل المكالمات:


 Request method: GET Request URI: http://localhost:8080/ Headers: Language=en Language=fr Accept=*/* Cookies: <none> Multiparts: <none> Body: <none> java.net.ConnectException: Connection refused (Connection refused) 

اتضح أنه تمت إضافة جميع الرؤوس إلى المكالمة ، ولكن أصبح URI فجأة مضيفًا محليًا - على الرغم من أنه تمت إضافته في المواصفات الأولى.


حدث هذا بسبب حقيقة أن REST Assured يعالج التخطيات لمعلمات الطلب بشكل مختلف (نفس الشيء مع الإجابة). تتم إضافة الرؤوس أو المرشحات إلى القائمة ثم يتم تطبيقها بدورها. يمكن أن يكون هناك URI واحد فقط ، لذلك يتم تطبيق آخر واحد. لم يتم تحديده في آخر مواصفات تم إضافتها - لذلك ، يقوم REST Assured بتجاوزها بالقيمة الافتراضية (المضيف المحلي).


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


REST الأساسي تكوين مضمون


هناك طريقة أخرى لاستعلامات النماذج في REST Assured وهي تكوين التكوين الأساسي وتحديد الحقول الثابتة للفئة RestAssured:


 @BeforeMethod public void configureRestAssured(...) { RestAssured.baseURI = "http://cookiemonster.com"; RestAssured.requestSpecification = given() .header("Language", "en"); RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); ... } 

سيتم إضافة القيم تلقائيًا إلى الطلب في كل مرة. يتم دمج التكوين مع التعليقات التوضيحية @BeforeMethod في @BeforeMethod و @BeforeEach في JUnit –- بحيث يمكنك التأكد من أن كل اختبار تقوم بتشغيله سيبدأ بنفس المعلمات.


ومع ذلك ، فإن التكوين سيكون مصدرا محتملا للمشاكل ، لأنه ثابت .


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


بقية مرشحات مضمونة


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


 public class OAuth2Filter implements AuthFilter { String accessToken; OAuth2Filter(String accessToken) { this.accessToken = accessToken; } @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) { requestSpec.replaceHeader("Authorization", "Bearer " + accessToken); return ctx.next(requestSpec, responseSpec); } } 

 String accessToken = getAccessToken(username, password); OAuth2Filter auth = new OAuth2Filter(accessToken); given() .filter(auth) .filter(new RequestLoggingFilter()) .filter(new ResponseLoggingFilter()) ... 

يتم تخزين عوامل التصفية التي تمت إضافتها إلى الطلب في LinkedList . قبل تقديم طلب ، يعدّل REST Assured ذلك من خلال الانتقال إلى القائمة وتطبيق مرشح واحد تلو الآخر. ثم يتم نفس الشيء مع الإجابة التي جاءت.


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


 given() .filter(auth) .filter(new RequestLoggingFilter()) … given() .filter(new RequestLoggingFilter()) .filter(auth) 

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


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


ليس فقط المرشحات


كيف نفعل إذن من خلال المرشحات هو مبين أعلاه. ولكن إلى جانب هذه الطريقة في REST Assured ، هناك طريقة أخرى ، من خلال AuthenticationScheme :


 String accessToken = getAccessToken(username, password); OAuth2Scheme scheme = new OAuth2Scheme(); scheme.setAccessToken(accessToken); RestAssured.authentication = scheme; 

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


مشكلة التبعية


تشير وثائق REST Assured إلى أنه لاستخدام Oauth1 أو Oauth2 (عن طريق تحديد رمز مميز كمعلمة استعلام) ، يجب إضافة التراخيص اعتمادًا على Scribe. ومع ذلك ، فإن استيراد أحدث إصدار لن يساعدك - ستواجه خطأً موصوفًا في إحدى المشكلات المفتوحة . يمكنك حلها فقط عن طريق استيراد الإصدار القديم من المكتبة ، 2.5.3. ومع ذلك ، في هذه الحالة سوف تواجه مشكلة أخرى .


بشكل عام ، لا يوجد إصدار آخر من Scribe يعمل مع Oauth2 REST Assured الإصدار 3.0.3 والإصدارات الأحدث (والإصدار الأخير 4.0.0 لم يصلح هذا).


التسجيل لا يعمل


يتم تطبيق المرشحات على الاستعلامات بترتيب معين. ويتم تطبيق AuthenticationScheme بعدهم. هذا يعني أنه سيكون من الصعب اكتشاف مشكلة في التخويل في الاختبار - لم يتم التعهد بذلك.


المزيد عن REST Assured بناء الجملة


يعني عدد كبير من الاختبارات أنها معقدة أيضًا. وإذا كانت واجهة برمجة التطبيقات هي الموضوع الرئيسي للاختبار ، وتحتاج إلى مراجعة ليس فقط حقول json ، ولكن منطق العمل ، ثم مع REST Assured ، يتحول الاختبار إلى ورقة:


 @Test public void shouldCorrectlyCountAddedCookies() { Integer addNumber = 10; JsonPath beforeCookies = given() .when() .get("/latestcookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); String beforeId = beforeCookies.getString("id"); JsonPath afterCookies = given() .body(String.format("{number: %s}", addNumber)) .when() .put("/cookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); Integer afterNumber = afterCookies.getInt("number"); String afterId = afterCookies.getString("id"); JsonPath history = given() .when() .get("/history") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", beforeId))) .isEqualTo(afterNumber - addNumber); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", afterId))) .isEqualTo(afterNumber); } 

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


given() ، when() then() يأخذ REST Assured من BDD ، مثل Spock أو خيار. ومع ذلك ، في الاختبارات المعقدة ، يُفقد معناها ، لأن مقياس الاختبار يصبح أكبر بكثير من طلب واحد - وهذا إجراء صغير يجب أن يُشار إليه بسطر واحد. ولهذا ، يمكنك نقل مكالمات REST Assured إلى الفصول المساعدة:


 public class CookieMonsterHelper { public static JsonPath getCookies() { return given() .when() .get("/cookiesformonster") .then() .extract() .jsonPath(); } ... } 

وندعو في الاختبار:


 JsonPath response = CookieMonsterHelper.getCookies(); 

من الجيد أن تكون مثل هذه الفئات المساعدة عالمية بحيث يمكن دمج الدعوة إلى إحدى الطرق في عدد كبير من الاختبارات - ثم يمكن وضعها في مكتبة منفصلة بشكل عام: فجأة ، تحتاج إلى استدعاء الطريقة في مرحلة ما في مشروع آخر. في هذه الحالة فقط ، سيتعين عليك إزالة كل التحقق من الاستجابة التي يمكن لـ Rest Assured القيام بها - بعد كل شيء ، غالبًا ما يمكن إرجاع بيانات مختلفة للغاية استجابةً لنفس الطلب.


استنتاج


REST Assured هي مكتبة للاختبار. إنها تعرف كيفية القيام بأمرين: إرسال الطلبات والتحقق من الإجابات. إذا حاولنا إزالته من الاختبارات وإزالة كل التحقق من الصحة ، فإنه يتحول إلى عميل HTTP .


إذا كان عليك كتابة عدد كبير من الاختبارات ومواصلة دعمها ، فكر فيما إذا كنت بحاجة إلى عميل HTTP مع بناء جملة مرهق ، وتهيئة ثابتة ، والارتباك في ترتيب تطبيق المرشحات والمواصفات ، وتسجيل يمكن كسرها بسهولة؟ ربما قبل تسع سنوات ، كانت REST Assured الأداة الأكثر ملاءمة ، لكن خلال هذا الوقت ظهرت بدائل - التحديثية ، Feign ، Unirest ، وما إلى ذلك - التي لا تحتوي على هذه الميزات.


تتجلى معظم المشكلات الموضحة في المقالة في مشاريع كبيرة. إذا كنت بحاجة إلى كتابة بعض الاختبارات بسرعة ونسيانها إلى الأبد ، ولا يحبها التحديثية ، فإن REST Assured هو الخيار الأفضل.


إذا كنت تكتب اختبارات بالفعل باستخدام REST Assured ، فليس من الضروري التعجيل بإعادة كتابة كل شيء. إذا كانت مستقرة وسريعة ، فسوف تقضي الكثير من وقتك أكثر مما ستحقق فوائد عملية. إذا لم يكن الأمر كذلك ، فإن REST Assured ليست مشكلتك الرئيسية.


كل يوم ، يتزايد عدد الاختبارات المكتوبة في DINS لواجهة برمجة تطبيقات RingCentral ، ولا تزال تستخدم REST Assured. إن مقدار الوقت الذي يجب إنفاقه للتبديل إلى عميل HTTP آخر ، على الأقل في اختبارات جديدة ، كبير جدًا ، وفئات المساعدة التي تم إنشاؤها وطرق تكوين تكوين الاختبار تحل معظم المشكلات. في هذه الحالة ، يعد الحفاظ على تكامل المشروع مع الاختبارات أكثر أهمية من استخدام العميل الأكثر جمالا وعصرية. طمأنة ، على الرغم من أوجه القصور ، يؤدي وظيفتها الرئيسية.

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


All Articles