خادم وهمية لأتمتة اختبار المحمول

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

تقليديا ، يتم استخدام المدرجات لاختبار مثل هذه التطبيقات. لكنهم لا يعملون دائمًا بشكل طبيعي ، وهذا يتعارض مع العمل. كحل بديل ، استخدمت moki. أريد الحديث عن هذا الطريق الشائك اليوم.

الصورة

من أجل عدم لمس رمز مشروع حقيقي (تحت NDA) ، لتوضيح المزيد من المناقشة ، قمت بإنشاء عميل REST بسيط لنظام Android ، والذي يسمح بإرسال طلبات HTTP (GET / POST) إلى عنوان معين مع المعلمات التي أحتاجها. سنقوم باختباره.
يمكن تنزيل رمز تطبيق العميل والمرسلين والاختبارات من GitLab .

ما هي الخيارات؟


هناك طريقتان للتدخين في حالتي:

  • نشر خادم وهمي في السحابة أو على جهاز بعيد (إذا كنا نتحدث عن التطورات السرية التي لا يمكن نقلها إلى السحابة) ؛
  • تشغيل خادم وهمية محليًا - مباشرة على الهاتف الذي يتم اختبار تطبيق الجوال عليه.

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

اختيار خادم وهمي


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

  • خادم وهمية ، wiremock - خادمان وهميان لا يمكنني تشغيلهما بشكل طبيعي على Android. نظرًا لأن جميع التجارب جرت كجزء من مشروع مباشر ، فإن وقت الاختيار كان محدودًا. بعد الحفر معهم بضعة أيام ، تخليت عن المحاولة.
  • Restmock عبارة عن غلاف فوق okhttpmockwebserver ، والذي سيتم مناقشته بمزيد من التفاصيل لاحقًا. بدا الأمر جيدًا ، ولكن بدأ مطور هذا الغلاف أخفى "تحت غطاء المحرك" القدرة على تعيين عنوان IP والمنفذ للخادم الوهمي ، وكان الأمر بالغ الأهمية بالنسبة لي. بدأ Restmock على بعض المنافذ العشوائية. بدت في الكود ، رأيت أنه عندما تمت تهيئة الخادم ، استخدم المطور طريقة لتعيين المنفذ بشكل عشوائي إذا لم يتلقه عند الإدخال. من حيث المبدأ ، يمكن للمرء أن يرث من هذه الطريقة ، ولكن المشكلة كانت في المنشئ الخاص. ونتيجة لذلك ، رفضت الغلاف.
  • Okhttpmockwebserver - بعد أن جربت أدوات مختلفة ، توقفت عند الخادم الوهمي ، والذي عادة ما يكون مجتمعة وبدأ محليًا على الجهاز.

نحن نحلل مبدأ العمل


يتيح لك الإصدار الحالي من okhttpmockwebserver تنفيذ العديد من سيناريوهات العمل:

  • قائمة الإجابات . تتم إضافة استجابات خادم وهمية إلى قائمة انتظار FIFO. لا يهم أي API وأي مسار سوف أقوم بالوصول إليه ، سيتناوب الخادم الوهمي في إلقاء الرسائل في قائمة الانتظار هذه.
  • يسمح لك المرسل بإنشاء قواعد تحدد الإجابة التي يجب تقديمها. افترض أن الطلب جاء من عنوان URL يحتوي على مسار ، على سبيل المثال / get-login /. على هذا الخادم / get-login / mock ويعطي استجابة واحدة محددة مسبقًا.
  • طلب التحقق . استنادًا إلى السيناريو السابق ، يمكنني التحقق من الطلبات التي يرسلها التطبيق (أنه في الظروف المحددة ، يترك طلبًا يحتوي على معلمات معينة حقًا). ومع ذلك ، فإن الإجابة غير مهمة ، حيث يتم تحديدها من خلال كيفية عمل API. يطبق هذا البرنامج النصي طلب التحقق.

فكر في كل من السيناريوهات بمزيد من التفصيل.

قائمة انتظار الاستجابة


أبسط تنفيذ لخادم وهمية هو قائمة انتظار الاستجابة. قبل الاختبار ، أحدد العنوان والمنفذ حيث سيتم نشر خادم وهمية ، بالإضافة إلى حقيقة أنه سيعمل على مبدأ قائمة انتظار الرسائل - FIFO (أولاً يخرج أولاً).

بعد ذلك ، قم بتشغيل خادم وهمية.

class QueueTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.enqueue(MockResponse().setBody("1st message")) mockServer.enqueue(MockResponse().setBody("2nd message")) mockServer.enqueue(MockResponse().setBody("3rd message")) mockServer.start(ip, port) } @Test fun queueTest() { sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("1st message") returnFromResponseActivity() sendPostRequest("http://localhost:8080/getMessage") assertResponseMessage("2nd message") returnFromResponseActivity() sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("3rd message") returnFromResponseActivity() } } 

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

تنفيذ المرسل


المرسل هو مجموعة من القواعد التي يعمل من خلالها خادم وهمية. من أجل الراحة ، أنشأت ثلاثة مرسلين مختلفين: SimpleDispatcher و OtherParamsDispatcher و ListingDispatcher.

مبسط


يوفر Okhttpmockwebserver فئة Dispatcher() لتطبيق المرسل. يمكنك أن ترث منه عن طريق تجاوز وظيفة dispatch الخاصة.

 class SimpleDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { if (request.method == "GET"){ return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""") } else if (request.method == "POST") { return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""") } return MockResponse().setResponseCode(200) } } 

المنطق في هذا المثال بسيط: إذا وصلت GET ، فأعيد رسالة مفادها أن هذا طلب GET. إذا كان POST ، فأنا أعيد رسالة حول طلب POST. في حالات أخرى ، أعيد طلبًا فارغًا.

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

يمكن العثور على مصادر الاختبار باستخدام SimpleDispatcher في المستودع .

OtherParams ديسباتشر


بتجاوز وظيفة dispatch ، يمكنني الدفع من معلمات الطلب الأخرى لإرسال الردود:

 class OtherParamsDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { return when { request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""") request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""") request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""") else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""") } } } 

في هذه الحالة ، أوضح عدة خيارات للظروف.

أولاً ، يمكنك تمرير المعلمات إلى واجهة برمجة التطبيقات في شريط العناوين. لذلك ، يمكنني وضع شرط على إدخال أي حزمة في المسار ، على سبيل المثال ، “?queryKey=value” .
ثانيًا ، يتيح لك هذا الفصل الدخول إلى جسم نص طلبات POST أو PUT. على سبيل المثال ، يمكنك استخدام contains تنفيذ toString() . في المثال الخاص بي ، يتم تشغيل الشرط عند وصول طلب POST الذي يحتوي على “bodyKey”:”value” . وبالمثل ، يمكنني التحقق من header : value الطلب ( header : value ).

للحصول على أمثلة من الاختبارات ، أوصي بالرجوع إلى المستودع .

الإدراج


إذا لزم الأمر ، يمكنك تنفيذ منطق أكثر تعقيدًا - ListingDispatcher. بنفس الطريقة ، تجاوزت وظيفة dispatch . ومع ذلك ، الآن في الفصل مباشرة ، قمت بتعيين مجموعة افتراضية من stubsList ( stubsList ) - stubsList مختلفة.

 class ListingDispatcher: Dispatcher() { private var stubsList: ArrayList<RequestClass> = defaultRequests() @Override override fun dispatch(request: RecordedRequest): MockResponse = try { stubsList.first { it.matcher(request.path, request.body.toString()) }.response() } catch (e: NoSuchElementException) { Log.e("Unexisting request path =", request.path) MockResponse().setResponseCode(404) } private fun defaultRequests(): ArrayList<RequestClass> { val allStubs = ArrayList<RequestClass>() allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }""")) return allStubs } fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } fun addMockStub(stub: RequestClass) { stubsList.add(stub) } } 

للقيام بذلك ، قمت بإنشاء فئة RequestClass مفتوحة ، جميع الحقول فارغة بشكل افتراضي. بالنسبة لهذه الفئة ، أقوم بتعريف دالة response تقوم بإنشاء كائن MockResponse (إرجاع 200 استجابة أو نص responseText ) ، ووظيفة matcher التي ترجع true أو false .

 open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") { open fun response(code: Int = 200): MockResponse = MockResponse() .setResponseCode(code) .setBody(responseText) open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body) } 

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

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

 fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } 

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

 class ListingDispatcherTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) private val dispatcher = ListingDispatcher() @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.setDispatcher(dispatcher) mockServer.start(ip, port) } . . . } 

من أجل التجربة ، استبدلت كعب مع POST:

 @Test fun postReplacedStubTest() { val params: HashMap<String, String> = hashMapOf("bodyParam" to "value") replacePostStub() sendPostRequest("http://localhost:8080/post", params = params) assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""") } 

للقيام بذلك ، تسمى وظيفة replacePostStub من dispatcher عادي وتضيف response جديدة.

 private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) } 

في الاختبار أعلاه ، أتحقق من أنه تم استبدال كعب الروتين.
ثم أضفت كعبًا جديدًا ، والذي لم يكن افتراضيًا.

 @Test fun getNewStubTest() { addSomeStub() sendGetRequest("http://localhost:8080/some_specific_url") assertResponseMessage("""{ "message" : "U have got specific message" }""") } 

 private fun addSomeStub() { dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }""")) } 

طلب التحقق


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

 @Test fun requestVerifierTest() { val params: HashMap<String, String> = hashMapOf("bodyKey" to "value") val headers: HashMap<String, String> = hashMapOf("header" to "value") sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method) assertEquals("value", request.getHeader("header")) assertTrue(request.body.toString().contains("\"bodyKey\":\"value\"")) assertTrue(request.path.startsWith("/post")) } 

أعلاه ، عرضت الاختبار بمثال بسيط. يمكن استخدام نفس النهج بالضبط لـ JSON المعقد ، بما في ذلك للتحقق من بنية الطلب بالكامل (يمكنك المقارنة على مستوى JSON أو تحليل JSON في الكائنات والتحقق من المساواة على مستوى الكائن).

ملخص


بشكل عام ، أحببت الأداة (okhttpmockwebserver) ، وأستخدمها في مشروع كبير. بالطبع ، هناك بعض الأشياء الصغيرة التي أود تغييرها.
على سبيل المثال ، لا أحب أن تضطر إلى النقر على العنوان المحلي (localhost: 8080 في مثالنا) في تكوينات تطبيقك ؛ ربما لا يزال بإمكاني العثور على طريقة لتهيئة كل شيء بحيث يستجيب الخادم الوهمي عند محاولة إرسال طلب إلى أي عنوان.
أيضًا ، ليس لدي القدرة على إعادة توجيه الطلبات - عندما يرسل الخادم الوهمي طلبًا إضافيًا ، إذا لم يكن لديه كعب روتين مناسب له. لا يوجد مثل هذا النهج في هذا الخادم الوهمي. ومع ذلك ، لم يأت حتى إلى تنفيذها ، حيث أن المشروع "القتالي" ليس لديه مثل هذه المهمة في الوقت الحالي.

كاتب المقال: رسلان عابدين

ملحوظة: نحن ننشر مقالاتنا على عدة مواقع في Runet. اشترك في صفحاتنا على VK أو FB أو قناة Telegram لمعرفة المزيد عن جميع منشوراتنا وأخبار Maxilect الأخرى.

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


All Articles