Tes sejarah proyek K: Kotlin & Spek

Halo, Habr!

Pada artikel ini kita akan berbicara tentang pengujian otomatis pada salah satu dari banyak proyek QIWI, yang diberi nama kode "K".



Ketika kami mengorganisir pengujian proyek ini, kami memutuskan untuk memilih Kotlin yang praktis dan hype, serta Spek , yang mengatakan "Anda menyebutnya pengujian, kami menyebutnya spesifikasi" (Anda menyebutnya pengujian, kami menyebutnya spesifikasi). Mungkin pendekatan ini cocok untuk Anda jika Anda dihadapkan dengan tugas serupa.

Kenapa Kotlin dan bukan yang lain? Kotlin dipilih oleh pengembangan, untuk bereksperimen, karena produk khusus ini tidak kritis, dan dimungkinkan untuk mempraktikkannya hidup tanpa rasa takut akan ada masalah.

Dokumentasi resmi memberi tahu kami bahwa "Spek ditulis di Kotlin, dan spesifikasi yang Anda tulis akan ditulis di Kotlin" - ini dengan sangat jelas menjawab pertanyaan: "Mengapa ini diperlukan?"

Jadi ...

Apa itu dan mengapa itu dibutuhkan?


Proyek ini menyediakan mitranya dengan perangkat lunak, yang merupakan aplikasi untuk Android. Bagian terbesar dari tes jatuh pada back-end, jadi kami akan fokus pada pengujian REST API.

Untuk sekelompok yang memungkinkan Anda untuk menulis tes dan mendapatkan hasil, semuanya jelas: Anda memerlukan bahasa pemrograman, kerangka kerja pengujian, klien HTTP dan laporan. Tetapi bagaimana dengan titik masuk ke alam semesta uji kita?

Persyaratan, mereka adalah spesifikasi, pengembang proyek memutuskan untuk menulis dalam bentuk tes. Hasilnya adalah gambar yang menarik - BDD. Dengan demikian, Kotlin, Spek dan khttp muncul di arena.
Pembaca yang penuh perhatian akan bertanya - OK, tetapi di mana penguji?

Penguji


Setelah menyelesaikan dua burung dengan satu batu, pengembangan memberi tester produk persyaratan dan autotest. Sejak itu, tester telah memperluas cakupan pengujian sesuai dengan persyaratan, dan juga mendukung dan membuat pengujian baru bersama dengan pengembang.

"Ini tidak bisa berlangsung selamanya dan tidak boleh berakhir tragis untuk proses pengujian!" - ketika ide semacam itu dikunjungi oleh rekan kerja, tim departemen layanan Departemen Pengujian memasuki permainan. Departemen layanan menghadapi tugas: mempelajari Kotlin dalam waktu singkat, sehingga jika perlu, lakukan tes cepat.

Memulai


Departemen layanan memiliki IntelliJ IDEA dalam layanan, dan karena Kotlin berjalan di atas JVM dan dikembangkan oleh JetBrains , tidak perlu menginstal apa pun tambahan untuk menulis kode.

Untuk alasan yang jelas, kami akan melewati proses belajar bahasa itu sendiri.

Hal pertama yang harus dimulai adalah mengkloning repositori:
git clone https://gerrit.project.com/k/autotests

Kemudian proyek dibuka dan pengaturan gradle diimpor :



Untuk kepuasan dan kenyamanan lengkap (* Sebenarnya, ini adalah suatu keharusan), plugin Spek dipasang:



Dia memastikan peluncuran tes di lingkungan pengembangan:



Tahap pertama selesai, dan sudah waktunya untuk mulai menulis tes sendiri.

Tes


Orang baik dari departemen layanan bukan milik produk ini atau itu. Ini adalah karyawan yang terburu-buru untuk membantu mengatur otomatisasi pada proyek, termasuk semua tahapan proses, dan kemudian lulus tes kepada penguji produk untuk dukungan dan commissioning.

Dan karena interaksi tim internal departemen pengujian diatur dengan cara yang serupa, maka departemen layanan โ€œmemintaโ€ setidaknya persyaratan untuk fitur yang akan dimasukkan.

Tampaknya ini jalan buntu dalam kasus "K". Tapi itu dia:

  • Baca akses ke repositori tempat sumber proyek disimpan diminta;
  • Mengkloning repositori;
  • Mereka mulai terjun ke fungsionalitas produk melalui membaca kode sumber yang ditulis dalam Java.

Apa yang kamu baca?


Pengembangan "K" diminta untuk menulis tes untuk fitur, yang memungkinkan Anda untuk menambah, memperbarui, dan menghapus produk yang dijual. Implementasinya terdiri dari dua bagian: "web" dan "mobile".

Dalam hal web:

  • Untuk menambahkan produk, permintaan POST digunakan, tubuh yang berisi JSON dengan data.
  • Untuk memperbarui atau mengedit produk, permintaan PUT digunakan, tubuh yang berisi JSON dengan data yang diubah.
  • Untuk menghapus barang, permintaan DELETE digunakan, yang tubuhnya kosong.

Dalam hal seluler:

Untuk menambah, memperbarui, dan menghapus produk, permintaan POST digunakan, tubuh yang berisi JSON dengan data untuk operasi yang ditentukan.

Yaitu JSON memiliki tiga node:

  • "Ditambahkan": daftar produk tambahan,
  • "Dihapus": daftar barang yang dihapus,
  • "Diperbarui": daftar produk yang diperbarui.

Apa yang kamu tulis?


Kelas uji yang berisi spesifikasi uji sudah dibuat dan berisi metode uji (* sedikit tidak di Spek), jadi itu hanya perlu diperluas.

Untuk web

Tes untuk penambahan produk yang sukses:

  • Tambahkan item
  • Periksa apakah item telah ditambahkan.
  • Hapus produk yang dibuat (kondisi akhir)

Kode:

 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) } 

Tes untuk menghilangkan barang dengan sukses:

  • Tambahkan produk (prasyarat)
  • Kami menghapus barang
  • Kami memeriksa bahwa produk telah dihapus

Kode:

 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` } } 

Tes negatif untuk eksekusi permintaan oleh pengguna yang tidak sah

  • Tambahkan item
  • Periksa status respons
  • Permintaan untuk menambahkan produk dikirim tanpa tajuk otorisasi. Jawabannya datang dengan 401 Status tidak sah.

Kode:

  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("") } } 

Untuk ponsel

Fungsi pembantu ditulis untuk mendapatkan node dari badan respons dan pembentukan badan permintaan.

Kode:

 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 ) ) } 

Tes untuk penambahan produk yang sukses

  • Tambahkan item
  • Periksa apakah item telah ditambahkan.
  • Kami menghapus barang (postcondition)

Kode:

 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))) } 

Tes untuk pembaruan produk yang sukses

  • Tambahkan produk (prasyarat)
  • Kami memperbarui barang
  • Pastikan item yang ditambahkan telah diperbarui.
  • Kami menghapus barang (postcondition)

Kode:

 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))) } 

Tes untuk menghilangkan barang dengan sukses:

  • Tambahkan produk (prasyarat)
  • Kami menghapus barang
  • Kami memverifikasi bahwa produk yang ditambahkan dihapus

Kode:

 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 } } 

Setelah menulis tes, perlu melalui kode review.

Ulasan


Lebih dari selusin komitmen, banyak korespondensi dengan dev, mengunjungi forum, berkomunikasi dengan Google. Dan inilah hasilnya.

Kode:

 package com.qiwi.k.tests.catalog import โ€ฆ class 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 } } } }) 

Ringkasan


Kode itu sendiri, keterampilan bahasa dan pengetahuan kerangka kerja jauh dari sempurna, tetapi awalnya umumnya tidak buruk.

Ketika saya bertemu Kotlin, saya merasa bahwa dia adalah gula sintaksis di Jawa. Dan ketika menulis kode dengan semua serat jiwa, saya berhasil merasakan kata-kata: "sepenuhnya kompatibel dengan Jawa".

Spek, yang menggunakan konstruksi bahasa sederhana untuk menggambarkan spesifikasi, menyediakan kumpulan metode pengujian yang lengkap. Yaitu memberikan apa yang mereka inginkan darinya dari kerangka uji.

Total - semua tes master. Semuanya berjalan dengan baik, dan departemen layanan sekarang tahu pasti bahwa itu akan dapat mendukung rekan-rekan dari K di masa-masa sulit.

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


All Articles