Prueba de historia del proyecto K: Kotlin & Spek

Hola Habr!

En este artículo hablaremos sobre las pruebas automáticas en uno de los muchos proyectos de QIWI, cuyo nombre en código es "K".



Cuando organizamos las pruebas de este proyecto, decidimos elegir el práctico y exagerado Kotlin , así como Spek , que dice "los llamas pruebas, los llamamos especificaciones" (los llamas pruebas, los llamamos especificaciones). Quizás este enfoque sea adecuado para usted si se enfrenta a tareas similares.

¿Por qué Kotlin y no otra cosa? Kotlin fue elegido por el desarrollo, para experimentar, ya que este producto en particular no era crítico, y era posible practicarlo en vivo sin temor a que hubiera problemas.

La documentación oficial nos dice que "Spek está escrito en Kotlin, y las especificaciones que escribes estarán escritas en Kotlin". Esto responde muy claramente a la pregunta: "¿Por qué es necesario?"

Entonces ...

¿Qué es y por qué se necesita?


El proyecto proporciona a su socio software, que es una aplicación para Android. La mayor parte de las pruebas recae en el back-end, por lo que nos centraremos en probar la API REST.

Para un grupo que le permite escribir pruebas y obtener resultados, todo está claro: necesita un lenguaje de programación, marco de prueba, cliente HTTP e informes. Pero, ¿qué pasa con el punto de entrada a nuestro universo de prueba?

Requisitos, son especificaciones, los desarrolladores del proyecto decidieron escribir en forma de pruebas. El resultado fue una imagen interesante: BDD. Así, Kotlin, Spek y Khttp aparecieron en la arena.
El lector atento preguntará: OK, pero ¿dónde están los evaluadores?

Probadores


Habiendo terminado dos pájaros de un tiro, el desarrollo le dio al probador del producto requisitos y pruebas automáticas. Desde entonces, el probador ha ampliado la cobertura de la prueba de acuerdo con los requisitos, y también admite y crea nuevas pruebas junto con los desarrolladores.

"¡Esto no puede continuar para siempre y no debe terminar trágicamente para el proceso de prueba!" - Cuando los colegas visitaron esa idea, el equipo del departamento de servicio del Departamento de Pruebas ingresó al juego. El departamento de servicio se enfrentó a la tarea: estudiar Kotlin en poco tiempo, de modo que, si fuera necesario, tomar el apoyo rápido de las pruebas.

Empezando


El departamento de servicio tiene IntelliJ IDEA en servicio, y dado que Kotlin se ejecuta sobre JVM y fue desarrollado por JetBrains , no hubo necesidad de instalar nada adicional para escribir código.

Por razones obvias, nos saltamos el proceso de aprender el lenguaje en sí.

Lo primero para comenzar fue clonar el repositorio:
git clone https://gerrit.project.com/k/autotests

Luego se abrió el proyecto y se importaron las configuraciones de gradle :



Para una completa satisfacción y comodidad (* En realidad, esto es imprescindible), se instaló el complemento Spek:



Aseguró el lanzamiento de pruebas en el entorno de desarrollo:



La primera etapa se completó, y era hora de comenzar a escribir las pruebas ellos mismos.

Pruebas


Los buenos del departamento de servicio no pertenecen a este o aquel producto. Estos son aquellos empleados que tienen prisa por ayudar a configurar la automatización del proyecto, incluidas todas las etapas del proceso, y luego pasan las pruebas a los probadores de productos para obtener asistencia y puesta en marcha.

Y dado que la interacción de los equipos internos del departamento de pruebas está organizada de manera similar, el departamento de servicio "pide" al menos los requisitos para ingresar a la función.

Puede parecer que este es un callejón sin salida en el caso de "K". Pero ahí estaba:

  • Se solicitó acceso de lectura al repositorio donde se almacenaron las fuentes del proyecto;
  • Clonado el repositorio;
  • Comenzaron a sumergirse en la funcionalidad del producto a través de la lectura de códigos fuente escritos en Java.

Que lees


El desarrollo "K" solicitó escribir pruebas para la función, lo que le permite agregar, actualizar y eliminar productos para la venta. La implementación consistió en dos partes: "web" y "móvil".

En el caso de la web:

  • Para agregar productos, se utiliza una solicitud POST, cuyo cuerpo contiene JSON con datos.
  • Para actualizar o editar productos, se utiliza una solicitud PUT, cuyo cuerpo contiene JSON con datos modificados.
  • Para eliminar mercancías, se utiliza una solicitud DELETE, cuyo cuerpo está vacío.

En el caso del móvil:

Para agregar, actualizar y eliminar productos, se utiliza una solicitud POST, cuyo cuerpo contiene JSON con datos para las operaciones especificadas.

Es decir JSON tiene tres nodos:

  • "Agregado": lista de productos agregados,
  • "Eliminado": una lista de elementos eliminados,
  • "Actualizado": una lista de productos actualizados.

Que escribiste


Ya se creó una clase de prueba que contenía especificaciones de prueba y contenía métodos de prueba (* un poco no en Spek), por lo que solo era necesario expandirla.

Para la web

Prueba para la adición exitosa del producto:

  • Agregar elemento
  • Verifique que el artículo haya sido agregado.
  • Eliminar el producto creado (condición posterior)

Código:

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

Prueba para la eliminación exitosa de bienes:

  • Añadir producto (condición previa)
  • Borramos los bienes
  • Verificamos que el producto ha sido eliminado

Código:

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

Prueba negativa para la ejecución de consultas por parte de usuarios no autorizados.

  • Agregar elemento
  • Verificar el estado de la respuesta
  • Se envía una solicitud para agregar un producto sin un encabezado de autorización. La respuesta viene con 401 estado no autorizado.

Código:

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

Para dispositivos móviles

Las funciones auxiliares se escribieron para obtener los nodos del cuerpo de respuesta y la formación del cuerpo de solicitud.

Código:

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

Prueba de adición exitosa del producto

  • Agregar elemento
  • Verifique que el artículo haya sido agregado.
  • Eliminamos los bienes (postcondición)

Código:

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

Prueba para una actualización exitosa del producto

  • Añadir producto (condición previa)
  • Actualizamos los bienes
  • Verifique que el elemento agregado haya sido actualizado.
  • Eliminamos los bienes (postcondición)

Código:

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

Prueba para la eliminación exitosa de bienes:

  • Añadir producto (condición previa)
  • Borramos los bienes
  • Verificamos que el producto agregado sea eliminado

Código:

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

Después de escribir las pruebas, fue necesario revisar el código.

Revisar


Más de una docena de confirmaciones, mucha correspondencia con desarrolladores, visitas a foros, comunicación con Google. Y aquí está el resultado.

Código:

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

Resumen


El código en sí, las habilidades lingüísticas y el conocimiento del marco están lejos de ser perfectos, pero el comienzo generalmente no es malo.

Cuando conocí a Kotlin, tuve la sensación de que era un azúcar sintáctico en Java. Y mientras escribía el código con todas las fibras del alma, logré sentir las palabras: "totalmente compatible con Java".

Spek, que utiliza construcciones de lenguaje simple para describir especificaciones, proporciona un conjunto completo de métodos de prueba. Es decir da lo que quieren de él como de un marco de prueba.

Total: todas las pruebas en master. Todo funcionó, y el departamento de servicio ahora sabe con certeza que podrá apoyar a colegas de K en tiempos difíciles.

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


All Articles