تاريخ اختبار مشروع K: Kotlin & Spek

مرحبا يا هبر!

في هذه المقالة سنتحدث عن الاختبار التلقائي في واحد من العديد من مشاريع QIWI ، التي تحمل الرمز "K".



عندما نظمنا اختبار هذا المشروع ، قررنا اختيار Kotlin العملي والضجيج ، بالإضافة إلى Spek ، الذي يقول: "تطلق عليهم الاختبارات ، نسميهم المواصفات" (تطلق عليهم الاختبارات ، نسميهم المواصفات). ربما يكون هذا النهج مناسبًا لك إذا كنت تواجه مهامًا مماثلة.

لماذا Kotlin وليس أي شيء آخر؟ تم اختيار Kotlin من قبل التطوير ، للتجربة ، لأن هذا المنتج المحدد لم يكن حرجًا ، وكان من الممكن ممارسته على الهواء دون خوف من أن تكون هناك مشاكل.

تخبرنا الوثائق الرسمية أن "سبيك مكتوبة بلغة كوتلن ، والمواصفات التي تكتبها ستكتب بلغة كوتلن" - وهذا يجيب بوضوح على السؤال: "لماذا هذا مطلوب؟"

لذا ...

ما هو ولماذا هو مطلوب؟


يوفر المشروع لشريكه برنامجًا ، وهو تطبيق لنظام Android. تقع حصة الأسد من الاختبارات في النهاية ، لذلك سنركز على اختبار REST API.

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

قرر مطورو المشروع المتطلبات ، فهي المواصفات ، والكتابة في شكل اختبارات. كانت النتيجة صورة مثيرة للاهتمام - BDD. وهكذا ، ظهر Kotlin و Spek و khttp في الساحة.
سوف يسأل القارئ اليقظ - حسنا ، ولكن أين هم المختبرون؟

اختبار


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

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

الشروع في العمل


لدى قسم الخدمة IntelliJ IDEA في الخدمة ، وبما أن Kotlin يعمل على رأس JVM وتم تطويره بواسطة JetBrains ، لم تكن هناك حاجة لتثبيت أي شيء إضافي لكتابة التعليمات البرمجية.

لأسباب واضحة ، سوف نتخطى عملية تعلم اللغة نفسها.

أول شيء بدأ به هو استنساخ المستودع:
git clone https://gerrit.project.com/k/autotests

ثم تم فتح المشروع واستيراد إعدادات التدرج :



للحصول على الرضا التام والراحة (* في الواقع ، هذا أمر لا بد منه) ، تم تثبيت المكون الإضافي Spek:



أكد إطلاق الاختبارات في بيئة التطوير:



تم الانتهاء من المرحلة الأولى ، وحان الوقت لبدء كتابة الاختبارات بأنفسهم.

الاختبارات


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

وبما أن تفاعل الفرق الداخلية لقسم الاختبار منظم بطريقة مماثلة ، فإن قسم الخدمة "يسأل" على الأقل عن متطلبات الميزة للدخول.

قد يبدو أن هذا طريق مسدود في حالة "K". ولكن كان هناك:

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

ماذا قرأت؟


طلب تطوير "K" لكتابة اختبارات للميزة ، والتي تسمح لك بإضافة وتحديث وحذف المنتجات للبيع. يتألف التنفيذ من جزأين: "الويب" و "الجوال".

في حالة الويب:

  • لإضافة منتجات ، يتم استخدام طلب POST ، والذي يحتوي نصه على JSON مع البيانات.
  • لتحديث المنتجات أو تعديلها ، يتم استخدام طلب PUT ، والذي يحتوي نصه على JSON مع البيانات التي تم تغييرها.
  • لحذف البضائع ، يتم استخدام طلب DELETE ، ويكون جسمه فارغًا.

في حالة الجوال:

لإضافة منتجات وتحديثها وحذفها ، يتم استخدام طلب POST ، والذي يحتوي نصه على JSON مع بيانات للعمليات المحددة.

على سبيل المثال يحتوي JSON على ثلاث عقد:

  • "مضاف": قائمة المنتجات المضافة ،
  • "تمت إزالته": قائمة العناصر المحذوفة ،
  • "محدث": قائمة المنتجات المحدثة.

ماذا كتبت؟


تم بالفعل إنشاء فئة اختبار تحتوي على مواصفات الاختبار وتحتوي على طرق اختبار (* القليل ليس في Spek) ، لذلك كان من الضروري فقط توسيعها.

للويب

اختبار لإضافة منتج ناجح:

  • أضف البند
  • تحقق من إضافة العنصر.
  • حذف المنتج الذي تم إنشاؤه (الحالة اللاحقة)

الكود:

 on("get changed since when goods added") { val goodsUpdateName: String = RandomStringUtils.randomAlphabetic(8) val date = Date.from(Instant.now()).time - 1 val goodsAdded = post(baseUrl + "goods/${user.storeId}", authHeader, json = dataToMap(goods.copy(name = goodsUpdateName))) it("should return the status code OK") { goodsAdded.statusCode.should.be.equal(OK) } val goodId = goodsAdded.jsonObject?.optLong("id") val goodsUpdates = get(baseUrl + "updates/goods/since/" + date, authHeader.plus("AppUID" to user.AppUID)) it("should return the status code OK") { goodsUpdates.statusCode.should.be.equal(OK) } val goodsInsert = goodsUpdates.jsonObject.getJSONArray("updated").toList() .map { it as JSONObject } .find { it.optLong("goodId") == goodId } it("should contain goods insert") { goodsInsert.should.be.not.`null` goodsInsert?.optString("name").should.be.equal(goodsUpdateName) } delete(baseUrl + "goods/${user.storeId}/$goodId", authHeader) } 

اختبار الإزالة الناجحة للبضائع:

  • إضافة منتج (شرط مسبق)
  • نحذف البضائع
  • نتحقق من حذف المنتج

الكود:

 on("get changed since when goods deleted") { val goodsUpdateName: String = RandomStringUtils.randomAlphabetic(8) val date = Date.from(Instant.now()).time - 1 val goodsAdded = post(baseUrl + "goods/${user.storeId}", authHeader, json = dataToMap(goods.copy(name = goodsUpdateName))) it("should return the status code OK") { goodsAdded.statusCode.should.be.equal(OK) } val goodId = goodsAdded.jsonObject?.optLong("id") val responseDelete = delete(baseUrl + "goods/${user.storeId}/$goodId", authHeader) it("should return the status code NO_CONTENT") { responseDelete.statusCode.should.be.equal(NO_CONTENT) } val goodsUpdates = get(baseUrl + "updates/goods/since/" + date, authHeader.plus("AppUID" to user.AppUID)) it("should contain goods deletes") { goodsUpdates.statusCode.should.be.equal(OK) goodsUpdates.jsonObject.getJSONArray("removed").toList() .map { it as Int } .find { it == goodId.toInt() } .should.be.not.`null` } } 

اختبار سلبي لتنفيذ الاستعلام بواسطة مستخدم غير مصرح به

  • أضف البند
  • تحقق من حالة الاستجابة
  • يتم إرسال طلب إضافة منتج بدون رأس تفويض. الجواب يأتي مع 401 حالة غير مصرح بها.

الكود:

  on("get changed since when goods added without authorization") { val response = post(baseUrl + "goods/${user.storeId}", json = dataToMap(goods)) it("should contain an Unauthorized response status and an empty body") { response.statusCode.should.be.equal(UNAUTHORIZED) response.text.should.be.equal("") } } 

للجوال

تم كتابة وظائف المساعد للحصول على العقد من هيئة الاستجابة وتشكيل هيئة الطلب.

الكود:

 package com.qiwi.k.tests import com.fasterxml.jackson.databind.ObjectMapper import khttp.responses.Response import org.json.JSONObject val mapper = ObjectMapper() fun arrayAdded(n: Int): Array<GoodsUpdate> { return Array(n) { i -> GoodsUpdate() } } fun getGoodsIds(list: List<GoodsUpdate>): List<Long> { return Array(list.size) { i -> list[i].goodId }.toList() } fun getResult(response: Response): List<GoodsUpdate> { return mapper.readValue( response.jsonObject.getJSONArray("result").toString(), Array<GoodsUpdate>::class.java ).toList() } fun getCountryIdFromTheResult(response: Response): List<Int> { val listGoods = mapper.readValue( response.jsonObject.getJSONArray("result").toString(), Array<GoodsUpdate>::class.java ).toList() return Array(listGoods.size) { i -> listGoods[i].countryId }.toList() } fun getBody(added: Array<GoodsUpdate> = emptyArray(), removed: List<Long> = emptyList(), updated: List<GoodsUpdate> = emptyList()): JSONObject { return JSONObject( mapOf( "added" to added, "removed" to removed, "updated" to updated ) ) } 

اختبار لإضافة منتج ناجح

  • أضف البند
  • تحقق من إضافة العنصر.
  • نحذف البضائع (شرط لاحق)

الكود:

 on("adding goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count") { resultOfAdding.should.be.size.equal(count) } post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding))) } 

اختبار لتحديث المنتج بنجاح

  • إضافة منتج (شرط مسبق)
  • نقوم بتحديث البضائع
  • تحقق من أنه تم تحديث العنصر المضاف.
  • نحذف البضائع (شرط لاحق)

الكود:

 on("updating goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code respAdd OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count (resultOfAdding)") { resultOfAdding.should.be.size.equal(count) } val respUpdate = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(updated = resultOfAdding.map { it.copy(countryId = REGION_77) }) ) it("should return the status code respUpdate OK") { respUpdate.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count (respUpdate)") { getResult(respUpdate).should.be.size.equal(count) } it("should be all elements are 77") { getCountryIdFromTheResult(respUpdate).should.be.all.elements(REGION_77) } post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding))) } 

اختبار الإزالة الناجحة للبضائع:

  • إضافة منتج (شرط مسبق)
  • نحذف البضائع
  • نتحقق من حذف المنتج المضاف

الكود:

 on("deleting goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code respAdd OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count") { resultOfAdding.should.be.size.equal(count) } val respRemoved = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding)) ) it("should return the status code respRemoved OK") { respRemoved.statusCode.should.be.equal(OK) } it("should be empty") { getResult(respRemoved).should.be.empty } } 

بعد كتابة الاختبارات ، كان من الضروري مراجعة التعليمات البرمجية.

مراجعة


أكثر من اثني عشر التزامًا ، والكثير من المراسلات مع ديف ، وزيارة المنتديات ، والتواصل مع Google. وهذه هي النتيجة.

الكود:

 package com.qiwi.k.tests.catalog importclass GoodsUpdatesControllerSpec : WebSpek({ given("GoodsUpdatesController") { val OK = HttpResponseStatus.OK.code() val NO_CONTENT = HttpResponseStatus.NO_CONTENT.code() val UNAUTHORIZED = HttpResponseStatus.UNAUTHORIZED.code() val REGION_77 = 77 val auth = login(user) val accessToken = auth.tokenHead + auth.tokenTail val authHeader = mapOf("Authorization" to "Bearer $accessToken") val baseUrl = "http://test.qiwi.com/catalog/" val count = 2 val authHeaderWithAppUID = mapOf("Authorization" to "Bearer $accessToken", "AppUID" to user.AppUID) val urlGoodsUpdate = "http://test.qiwi.com/catalog/updates/goods/" on("get changes since") { val goodsName: String = goodsForUpdate.name + Random().nextInt(1000) val date = Date.from(Instant.now()).time - 1 put(baseUrl + "goods/${user.storeId}", authHeader, json = dataToMap(goodsForUpdate.copy(name = goodsName))) val goodsUpdates = get(baseUrl + "updates/goods/since/" + date, authHeader.plus("AppUID" to user.AppUID)) it("should contain goods updates") { val updates = goodsUpdates.jsonObject.getJSONArray("updated").toList() .map { it as JSONObject } .find { it.optLong("goodId") == goodsForUpdate.id } updates.should.be.not.`null` updates?.optString("name").should.be.equal(goodsName) } } on("get changed since when goods added") { val goodsUpdateName: String = RandomStringUtils.randomAlphabetic(8) val date = Date.from(Instant.now()).time - 1 val goodsAdded = post(baseUrl + "goods/${user.storeId}", authHeader, json = dataToMap(goods.copy(name = goodsUpdateName))) it("should return the status code OK") { goodsAdded.statusCode.should.be.equal(OK) } val goodId = goodsAdded.jsonObject?.optLong("id") val goodsUpdates = get(baseUrl + "updates/goods/since/" + date, authHeader.plus("AppUID" to user.AppUID)) it("should return the status code OK") { goodsUpdates.statusCode.should.be.equal(OK) } val goodsInsert = goodsUpdates.jsonObject.getJSONArray("updated").toList() .map { it as JSONObject } .find { it.optLong("goodId") == goodId } it("should contain goods insert") { goodsInsert.should.be.not.`null` goodsInsert?.optString("name").should.be.equal(goodsUpdateName) } delete(baseUrl + "goods/${user.storeId}/$goodId", authHeader) } on("get changed since when goods deleted") { val goodsUpdateName: String = RandomStringUtils.randomAlphabetic(8) val date = Date.from(Instant.now()).time - 1 val goodsAdded = post(baseUrl + "goods/${user.storeId}", authHeader, json = dataToMap(goods.copy(name = goodsUpdateName))) it("should return the status code OK") { goodsAdded.statusCode.should.be.equal(OK) } val goodId = goodsAdded.jsonObject?.optLong("id") val responseDelete = delete(baseUrl + "goods/${user.storeId}/$goodId", authHeader) it("should return the status code NO_CONTENT") { responseDelete.statusCode.should.be.equal(NO_CONTENT) } val goodsUpdates = get(baseUrl + "updates/goods/since/" + date, authHeader.plus("AppUID" to user.AppUID)) it("should contain goods deletes") { goodsUpdates.statusCode.should.be.equal(OK) goodsUpdates.jsonObject.getJSONArray("removed").toList() .map { it as Int } .find { it == goodId.toInt() } .should.be.not.`null` } } on("get changed since when goods added without authorization") { val response = post(baseUrl + "goods/${user.storeId}", json = dataToMap(goods)) it("should contain an Unauthorized response status and an empty body") { response.statusCode.should.be.equal(UNAUTHORIZED) response.text.should.be.equal("") } } on("adding goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count") { resultOfAdding.should.be.size.equal(count) } post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding))) } on("updating goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code respAdd OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count (resultOfAdding)") { resultOfAdding.should.be.size.equal(count) } val respUpdate = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(updated = resultOfAdding.map { it.copy(countryId = REGION_77) }) ) it("should return the status code respUpdate OK") { respUpdate.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count (respUpdate)") { getResult(respUpdate).should.be.size.equal(count) } it("should be all elements are 77") { getCountryIdFromTheResult(respUpdate).should.be.all.elements(REGION_77) } post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding))) } on("deleting goods") { val respAdd = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(added = arrayAdded(count))) val resultOfAdding = getResult(respAdd) it("should return the status code respAdd OK") { respAdd.statusCode.should.be.equal(OK) } it("should be equal to the size of the variable count") { resultOfAdding.should.be.size.equal(count) } val respRemoved = post(urlGoodsUpdate, authHeaderWithAppUID, json = getBody(removed = getGoodsIds(resultOfAdding)) ) it("should return the status code respRemoved OK") { respRemoved.statusCode.should.be.equal(OK) } it("should be empty") { getResult(respRemoved).should.be.empty } } } }) 

الملخص


الكود نفسه ومهارات اللغة ومعرفة الإطار بعيدة عن الكمال ، ولكن البداية ليست سيئة بشكل عام.

عندما قابلت Kotlin ، شعرت أنه كان سكرًا نحويًا في Java. وأثناء كتابة الرمز مع كل ألياف الروح ، تمكنت من الشعور بالكلمات: "متوافقة تمامًا مع Java".

يوفر Spek ، الذي يستخدم تركيبات لغة بسيطة لوصف المواصفات ، مجموعة كاملة من طرق الاختبار. على سبيل المثال يعطي ما يريدون منه من إطار اختبار.

المجموع - جميع الاختبارات في الماجستير. لقد نجح كل شيء ، ويعرف قسم الخدمة الآن على وجه اليقين أنه سيكون قادرًا على دعم الزملاء من K في الأوقات الصعبة.

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


All Articles