Servidor simulado para la automatización de pruebas móviles

Mientras trabajaba en el último proyecto, me enfrenté a probar una aplicación móvil conectada al nivel de lógica de negocios con varios servicios de terceros. Sin embargo, probar estos servicios no era parte de mi tarea, los problemas con su API bloquearon el trabajo de la aplicación en sí misma: las pruebas cayeron no debido a problemas en el interior, sino debido a la inoperancia de la API, incluso antes de llegar a la verificación de la funcionalidad necesaria.

Tradicionalmente, los soportes se utilizan para probar tales aplicaciones. Pero no siempre funcionan normalmente, y esto interfiere con el trabajo. Como solución alternativa, utilicé moki. Quiero hablar sobre este camino espinoso hoy.

imagen

Para no tocar el código de un proyecto real (bajo NDA), para mayor claridad, creé un cliente REST simple para Android, que permite enviar solicitudes HTTP (GET / POST) a una determinada dirección con los parámetros que necesito. Lo probaremos
El código de aplicación del cliente, los despachadores y las pruebas se pueden descargar desde GitLab .

Cuales son las opciones?


Hubo dos enfoques para burlarse en mi caso:

  • implementar un servidor simulado en la nube o en una máquina remota (si estamos hablando de desarrollos confidenciales que no pueden llevarse a la nube);
  • inicie el servidor simulado localmente, directamente en el teléfono en el que se está probando la aplicación móvil.

La primera opción no es muy diferente del banco de pruebas. De hecho, es posible asignar una estación de trabajo en la red para el servidor simulado, pero deberá ser compatible, como cualquier banco de pruebas. Y aquí es necesario encontrar las principales trampas de este enfoque. La estación de trabajo remota ha muerto, dejó de responder, algo ha cambiado: debe monitorear, cambiar la configuración, es decir haga lo mismo que con el apoyo de un banco de pruebas regular. No podemos corregir la situación por nosotros mismos, y ciertamente tomará más tiempo que cualquier manipulación local. Entonces, específicamente en mi proyecto, fue más conveniente aumentar el servidor simulado localmente.

Elegir un servidor simulado


Hay muchas herramientas diferentes. Traté de trabajar con varios y en casi todos me encontré con ciertos problemas:

  • Mock-server , wiremock : dos servidores simulados que no podría ejecutar normalmente en Android. Como todos los experimentos se llevaron a cabo como parte de un proyecto en vivo, el tiempo de elección fue limitado. Después de cavar con ellos un par de días, dejé de intentarlo.
  • Restmock es un contenedor sobre okhttpmockwebserver , que se discutirá con más detalle más adelante. Se veía bien, comenzó, pero el desarrollador de este contenedor ocultó "bajo el capó" la capacidad de establecer la dirección IP y el puerto del servidor simulado, y para mí fue crítico. Restmock comenzó en algún puerto aleatorio. Al hurgar en el código, vi que cuando el servidor se inicializaba, el desarrollador usaba un método que configuraba el puerto al azar si no lo recibía en la entrada. En principio, uno podría heredar de este método, pero el problema estaba en el constructor privado. Como resultado, rechacé el envoltorio.
  • Okhttpmockwebserver : después de probar diferentes herramientas, me detuve en el servidor simulado, que normalmente se juntaba y comenzaba localmente en el dispositivo.

Analizamos el principio del trabajo.


La versión actual de okhttpmockwebserver le permite implementar varios escenarios de trabajo:

  • Cola de respuestas . Las respuestas falsas del servidor se agregan a la cola FIFO. No importa a qué API y qué ruta accederé, el servidor simulado se turnará para lanzar mensajes en esta cola.
  • El despachador le permite crear reglas que determinan qué respuesta dar. Supongamos que una solicitud proviene de una URL que contiene una ruta, por ejemplo / get-login /. En este servidor / get-login / simulacro y da una respuesta única y predefinida.
  • Verificador de solicitud . Según el escenario anterior, puedo verificar las solicitudes que envía la aplicación (que en las condiciones dadas, una solicitud con ciertos parámetros realmente se va). Sin embargo, la respuesta no es importante, ya que está determinada por cómo funciona la API. Este script implementa el verificador de solicitud.

Considere cada uno de los escenarios con más detalle.

Cola de respuesta


La implementación más simple del servidor simulado es la cola de respuesta. Antes de la prueba, determino la dirección y el puerto donde se implementará el servidor simulado, así como el hecho de que funcionará según el principio de una cola de mensajes: FIFO (primero en entrar, primero en salir).

A continuación, ejecute el servidor simulado.

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

Las pruebas se escriben utilizando el marco Espresso, diseñado para realizar acciones en aplicaciones móviles. En este ejemplo, selecciono los tipos de solicitud y los envío por turno.
Después de comenzar la prueba, el servidor simulado le da respuestas de acuerdo con la cola prescrita, y la prueba pasa sin errores.

Implementación del despachador


Un despachador es un conjunto de reglas por las cuales opera un servidor simulado. Por conveniencia, creé tres despachadores diferentes: SimpleDispatcher, OtherParamsDispatcher y ListingDispatcher.

Despachador simple


Okhttpmockwebserver proporciona la clase Dispatcher() para implementar el despachador. Puede heredar de él anulando la función de dispatch a su manera.

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

La lógica en este ejemplo es simple: si llega un GET, devuelvo un mensaje de que se trata de una solicitud GET. Si es POST, devuelvo un mensaje sobre la solicitud POST. En otras situaciones, devuelvo una solicitud vacía.

SimpleDispatcher aparece en la prueba, un objeto de la clase SimpleDispatcher , que describí anteriormente. Además, como en el ejemplo anterior, se lanza el servidor simulado, solo que esta vez se indica un tipo de regla para trabajar con este servidor simulado: el mismo despachador.

Las fuentes de prueba con SimpleDispatcher se pueden encontrar en el repositorio .

OtherParamsDispatcher


Al anular la función de dispatch , puedo alejarme de otros parámetros de solicitud para enviar respuestas:

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

En este caso, demuestro varias opciones para las condiciones.

En primer lugar, puede pasar parámetros a la API en la barra de direcciones. Por lo tanto, puedo poner una condición en la entrada de cualquier paquete en la ruta, por ejemplo, “?queryKey=value” .
En segundo lugar, esta clase le permite acceder al cuerpo del cuerpo de las solicitudes POST o PUT. Por ejemplo, puede usar contains ejecutando primero toString() . En mi ejemplo, la condición se activa cuando “bodyKey”:”value” una solicitud POST que contiene “bodyKey”:”value” . Del mismo modo, puedo validar el header : value la solicitud ( header : value ).

Para ver ejemplos de pruebas, recomiendo consultar el repositorio .

ListingDispatcher


Si es necesario, puede implementar una lógica más compleja: ListingDispatcher. Del mismo modo, anulo la función de dispatch . Sin embargo, ahora mismo en la clase configuré el conjunto predeterminado de stubsList ( stubsList ) - mok para diferentes ocasiones.

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

Para hacer esto, creé una clase abierta RequestClass , todos los campos están vacíos por defecto. Para esta clase, defino una función de response que crea un objeto MockResponse (que devuelve una respuesta 200 o algún otro texto de responseText ) y una función de matcher que devuelve true o 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) } 

Como resultado, puedo construir combinaciones de condiciones más complejas para los talones. Este diseño me pareció más flexible, aunque el principio en su núcleo es muy simple.

Pero, sobre todo en este enfoque, me gustó poder sustituir algunos stubs sobre la marcha, si es necesario cambiar algo en la respuesta del servidor simulado en una prueba. Al probar proyectos grandes, este problema surge con bastante frecuencia, por ejemplo, al verificar algunos escenarios específicos.
El reemplazo se puede hacer de la siguiente manera:

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

Con esta implementación del despachador, las pruebas siguen siendo simples. También inicio el servidor simulado, solo selecciono 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) } . . . } 

Por el bien del experimento, reemplacé el trozo con 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" }""") } 

Para hacer esto, llamó a la función replacePostStub desde un dispatcher regular y agregó una nueva response .

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

En la prueba anterior, verifico que el trozo ha sido reemplazado.
Luego agregué un nuevo código auxiliar, que no estaba predeterminado.

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

Verificador de solicitud


El último caso, el verificador de solicitudes, no proporciona espionaje, pero verifica las solicitudes enviadas por la aplicación. Para hacer esto, acabo de iniciar el servidor simulado implementando el despachador para que la aplicación devuelva al menos algo.
Al enviar una solicitud de una prueba, llega al servidor simulado. A través de él, puedo acceder a los parámetros de solicitud usando 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")) } 

Arriba, mostré la prueba con un simple ejemplo. Se puede usar exactamente el mismo enfoque para JSON complejo, incluso para verificar toda la estructura de la solicitud (puede comparar a nivel JSON o analizar JSON en objetos y verificar la igualdad a nivel de objeto).

Resumen


En general, me gustó la herramienta (okhttpmockwebserver), y la uso en un proyecto grande. Por supuesto, hay algunas pequeñas cosas que me gustaría cambiar.
Por ejemplo, no me gusta que deba llamar a la dirección local (localhost: 8080 en nuestro ejemplo) en las configuraciones de su aplicación; tal vez todavía pueda encontrar una manera de configurar todo para que el servidor falso responda al intentar enviar una solicitud a cualquier dirección.
Además, no tengo la capacidad de redirigir solicitudes, cuando el servidor simulado envía una solicitud adicional, si no tiene un código auxiliar adecuado para ello. No existe tal enfoque en este servidor simulado. Sin embargo, ni siquiera llegó a su implementación, ya que en este momento el proyecto de "combate" no tiene esa tarea.

Autor del artículo: Ruslan Abdulin

PD: publicamos nuestros artículos en varios sitios de Runet. Suscríbase a nuestras páginas en VK , FB o Telegram-channel para conocer todas nuestras publicaciones y otras noticias de Maxilect.

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


All Articles