Saat mengerjakan proyek terbaru, saya dihadapkan dengan pengujian aplikasi seluler yang terhubung pada tingkat logika bisnis dengan berbagai layanan pihak ketiga. Menguji layanan ini bukan bagian dari tugas saya, namun, masalah dengan API mereka memblokir pekerjaan aplikasi itu sendiri - tes jatuh bukan karena masalah di dalam, tetapi karena tidak dapat dioperasikannya API, bahkan sebelum mencapai pemeriksaan untuk fungsionalitas yang diperlukan.
Secara tradisional, stan digunakan untuk menguji aplikasi semacam itu. Tetapi mereka tidak selalu bekerja secara normal, dan ini mengganggu pekerjaan. Sebagai solusi alternatif, saya menggunakan moki. Saya ingin berbicara tentang jalan yang sulit ini hari ini.

Agar tidak menyentuh kode proyek nyata (di bawah NDA), untuk kejelasan diskusi lebih lanjut, saya membuat klien REST sederhana untuk Android, yang memungkinkan pengiriman permintaan HTTP (GET / POST) ke alamat tertentu dengan parameter yang saya butuhkan. Kami akan mengujinya.
Kode aplikasi, dispatcher, dan tes klien dapat
diunduh dari GitLab .
Apa saja pilihannya?
Ada dua pendekatan untuk menghisap dalam kasus saya:
- menyebarkan server tiruan di cloud atau di mesin jarak jauh (jika kita berbicara tentang perkembangan rahasia yang tidak dapat dibawa ke cloud);
- luncurkan server tiruan secara lokal - langsung di ponsel tempat aplikasi seluler sedang diuji.
Opsi pertama tidak jauh berbeda dari bangku tes. Memang, dimungkinkan untuk mengalokasikan workstation di jaringan untuk server tiruan, tetapi perlu didukung, seperti test stand apa pun. Dan di sini perlu untuk menemukan perangkap utama dari pendekatan ini. Stasiun kerja jarak jauh telah mati, berhenti merespons, sesuatu telah berubah - Anda perlu memantau, mengubah konfigurasi, mis. lakukan semua sama seperti dengan dukungan bangku tes reguler. Kami tidak dapat memperbaiki situasi untuk diri kami sendiri, dan tentu saja akan memakan waktu lebih lama daripada manipulasi lokal. Jadi secara khusus dalam proyek saya, lebih mudah untuk meningkatkan server tiruan secara lokal.
Memilih Server Mock
Ada banyak alat berbeda. Saya mencoba bekerja dengan beberapa orang dan pada hampir semua orang saya mengalami masalah tertentu:
- Mock-server , wiremock - dua mock-server, yang masih belum bisa saya jalankan dengan normal di Android. Karena semua percobaan berlangsung sebagai bagian dari proyek langsung, waktu untuk pilihan terbatas. Setelah menggali bersama mereka beberapa hari, saya menyerah mencoba.
- Restmock adalah pembungkus di atas okhttpmockwebserver , yang akan dibahas lebih detail nanti. Itu terlihat bagus, itu dimulai, tetapi pengembang bungkus ini menyembunyikan "di balik tudung" kemampuan untuk mengatur alamat IP dan port server tiruan, dan bagi saya itu sangat penting. Restmock dimulai pada beberapa port acak. Menyodok dalam kode, saya melihat bahwa ketika server diinisialisasi, pengembang menggunakan metode yang mengatur port secara acak jika tidak menerimanya pada input. Pada prinsipnya, seseorang dapat mewarisi dari metode ini, tetapi masalahnya ada di konstruktor pribadi. Akibatnya, saya menolak bungkusnya.
- Okhttpmockwebserver - setelah mencoba berbagai alat, saya berhenti di server tiruan, yang biasanya berkumpul dan mulai secara lokal pada perangkat.
Kami menganalisis prinsip kerja
Versi okhttpmockwebserver saat ini memungkinkan Anda untuk mengimplementasikan beberapa skenario kerja:
- Antrian jawaban . Respons server tiruan ditambahkan ke antrian FIFO. Tidak masalah API mana dan jalur mana yang akan saya akses, server tiruan akan bergantian melempar pesan dalam antrian ini.
- Dispatcher memungkinkan Anda membuat aturan yang menentukan jawaban mana yang akan diberikan. Misalkan permintaan datang dari URL yang berisi jalur, misalnya / dapatkan-login /. Pada server / get-login / tiruan ini dan berikan satu respons yang sudah ditentukan sebelumnya.
- Meminta Verifikasi . Berdasarkan skenario sebelumnya, saya dapat memeriksa permintaan yang dikirim aplikasi (bahwa dalam kondisi yang diberikan, permintaan dengan parameter tertentu benar-benar pergi). Namun, jawabannya tidak penting, karena ditentukan oleh cara kerja API. Script ini mengimplementasikan verifikasi Permintaan.
Pertimbangkan masing-masing skenario lebih terinci.
Antrian respons
Implementasi paling sederhana dari server tiruan adalah antrian respons. Sebelum pengujian, saya menentukan alamat dan port di mana server tiruan akan digunakan, serta fakta bahwa itu akan bekerja pada prinsip antrian pesan - FIFO (first in first out).
Selanjutnya, jalankan server tiruan.
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() } }
Tes ditulis menggunakan kerangka Espresso, yang dirancang untuk melakukan tindakan dalam aplikasi seluler. Dalam contoh ini, saya memilih jenis permintaan dan mengirimkannya secara bergantian.
Setelah memulai tes, server tiruan memberikan jawaban sesuai dengan antrian yang ditentukan, dan tes berlalu tanpa kesalahan.
Implementasi operator
Dispatcher adalah seperangkat aturan yang digunakan oleh server tiruan. Untuk kenyamanan, saya membuat tiga operator berbeda: SimpleDispatcher, OtherParamsDispatcher, dan ListingDispatcher.
Simpledispatcher
Okhttpmockwebserver menyediakan kelas
Dispatcher()
untuk mengimplementasikan dispatcher. Anda dapat mewarisinya dengan mengganti fungsi
dispatch
dengan cara Anda sendiri.
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) } }
Logika dalam contoh ini sederhana: jika GET tiba, saya mengembalikan pesan bahwa ini adalah permintaan GET. Jika POST, saya mengembalikan pesan tentang permintaan POST. Dalam situasi lain, saya mengembalikan permintaan kosong.
SimpleDispatcher
muncul dalam tes - objek dari kelas
SimpleDispatcher
, yang saya jelaskan di atas. Selanjutnya, seperti pada contoh sebelumnya, server tiruan diluncurkan, hanya kali ini semacam aturan untuk bekerja dengan server tiruan ini diindikasikan - operator yang sama.
Sumber pengujian dengan
SimpleDispatcher
dapat ditemukan
di repositori .
Dispatcher Parram lain
Mengganti fungsi
dispatch
, saya dapat mendorong dari parameter permintaan lain untuk mengirim tanggapan:
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 }""") } } }
Dalam hal ini, saya mendemonstrasikan beberapa opsi untuk kondisi.
Pertama, Anda dapat meneruskan parameter ke API di bilah alamat. Oleh karena itu, saya dapat memberi syarat pada entri bundel apa pun di jalur, misalnya,
β?queryKey=valueβ
.
Kedua, kelas ini memungkinkan Anda untuk masuk ke dalam tubuh permintaan POST atau PUT. Misalnya, Anda dapat menggunakan
contains
dengan terlebih dahulu menjalankan
toString()
. Dalam contoh saya, kondisi dipicu ketika permintaan POST yang berisi
βbodyKeyβ:βvalueβ
. Demikian pula, saya dapat memvalidasi
header : value
permintaan (
header : value
).
Untuk contoh tes, saya sarankan merujuk
ke repositori .
ListingDispatcher
Jika perlu, Anda dapat menerapkan logika yang lebih kompleks - ListingDispatcher. Dengan cara yang sama, saya mengganti fungsi
dispatch
. Namun, sekarang tepat di kelas saya mengatur set standar
stubsList
(
stubsList
) -
stubsList
untuk berbagai kesempatan.
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) } }
Untuk melakukan ini, saya membuat
RequestClass
kelas terbuka, semua bidang yang kosong secara default. Untuk kelas ini, saya mendefinisikan fungsi
response
yang membuat objek
MockResponse
(mengembalikan 200 respons atau
responseText
lainnya), dan fungsi
matcher
yang mengembalikan
true
atau
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) }
Sebagai hasilnya, saya dapat membangun kombinasi kondisi yang lebih kompleks untuk bertopik. Desain ini menurut saya lebih fleksibel, meskipun prinsip dasarnya sangat sederhana.
Tetapi yang paling utama dalam pendekatan ini, saya suka bahwa saya dapat mengganti beberapa bertopik di mana saja, jika ada kebutuhan untuk mengubah sesuatu dalam respon server tiruan pada satu tes. Saat menguji proyek besar, masalah ini cukup sering muncul, misalnya, ketika memeriksa beberapa skenario tertentu.
Penggantian dapat dilakukan sebagai berikut:
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) }
Dengan implementasi dispatcher ini, pengujiannya tetap sederhana. Saya juga memulai server tiruan, hanya pilih
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) } . . . }
Demi percobaan, saya mengganti rintisan dengan 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" }""") }
Untuk melakukan ini, disebut fungsi
replacePostStub
dari
dispatcher
biasa dan menambahkan
response
baru.
private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) }
Dalam tes di atas, saya memverifikasi bahwa rintisan telah diganti.
Kemudian saya menambahkan rintisan baru, yang tidak ada dalam standar.
@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" }""")) }
Meminta Verifikasi
Kasus terakhir - Permintaan verifikasi - tidak menyediakan untuk pengintaian, tetapi memeriksa permintaan yang dikirim oleh aplikasi. Untuk melakukan ini, saya baru saja memulai server tiruan dengan mengimplementasikan dispatcher sehingga aplikasi mengembalikan setidaknya sesuatu.
Saat mengirim permintaan dari tes, itu datang ke server tiruan. Melalui itu, saya dapat mengakses parameter permintaan menggunakan
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")) }
Di atas, saya menunjukkan tes dengan contoh sederhana. Pendekatan yang persis sama dapat digunakan untuk JSON kompleks, termasuk untuk memeriksa seluruh struktur permintaan (Anda dapat membandingkan di tingkat JSON atau mengurai JSON menjadi objek dan memeriksa kesetaraan di tingkat objek).
Ringkasan
Secara umum, saya menyukai alat ini (okhttpmockwebserver), dan saya menggunakannya pada proyek besar. Tentu saja, ada beberapa hal kecil yang ingin saya ubah.
Misalnya, saya tidak suka Anda harus mengetuk alamat lokal (localhost: 8080 dalam contoh kami) di konfigurasi aplikasi Anda; mungkin saya masih dapat menemukan cara untuk mengkonfigurasi semuanya sehingga server tiruan merespons ketika mencoba mengirim permintaan ke alamat apa pun.
Juga, saya tidak memiliki kemampuan untuk mengarahkan permintaan - ketika server tiruan mengirim permintaan lebih lanjut, jika tidak memiliki tulisan rintisan yang cocok untuk itu. Tidak ada pendekatan seperti itu di server tiruan ini. Namun, itu bahkan tidak sampai pada implementasi mereka, karena saat ini proyek "pertempuran" tidak memiliki tugas seperti itu.
Penulis artikel: Ruslan Abdulin
PS Kami menerbitkan artikel kami di beberapa situs Runet. Berlangganan ke halaman kami di
VK ,
FB atau
saluran Telegram untuk mencari tahu tentang semua publikasi kami dan berita Maxilect lainnya.