Historique des tests du projet K: Kotlin & Spek

Bonjour, Habr!

Dans cet article, nous parlerons des tests automatiques sur l'un des nombreux projets QIWI, nommé "K".



Lorsque nous avons organisé les tests de ce projet, nous avons décidé de choisir le Kotlin pratique et hype, ainsi que Spek , qui dit «Vous les appelez tests, nous les appelons spécifications» (vous les appelez tests, nous les appelons spécifications). Peut-être que cette approche vous convient si vous êtes confronté à des tâches similaires.

Pourquoi Kotlin et pas autre chose? Kotlin a été choisi par le développement, pour expérimenter, car ce produit particulier n'était pas critique, et il était possible de le pratiquer en direct sans crainte de problèmes.

La documentation officielle nous dit que "Spek est écrit en Kotlin, et les spécifications que vous écrivez seront écrites en Kotlin" - cela répond très clairement à la question: "Pourquoi est-ce nécessaire?"

Alors ...

Qu'est-ce que c'est et pourquoi est-il nécessaire?


Le projet fournit à son partenaire un logiciel, qui est une application pour Android. La part du lion des tests tombe sur le back-end, nous allons donc nous concentrer sur les tests de l'API REST.

Pour un groupe qui vous permet d'écrire des tests et d'obtenir des résultats, tout est clair: vous avez besoin d'un langage de programmation, d'un framework de test, d'un client HTTP et de rapports. Mais qu'en est-il du point d'entrée dans notre univers de test?

Exigences, ce sont des spécifications, les développeurs du projet ont décidé d'écrire sous forme de tests. Le résultat était une image intéressante - BDD. Ainsi, Kotlin, Spek et khttp sont apparus dans l'arène.
Le lecteur attentif demandera - OK, mais où sont les testeurs?

Testeurs


Après avoir terminé deux oiseaux avec une pierre, le développement a donné au testeur de produit à la fois les exigences et les autotests. Depuis lors, le testeur a étendu la couverture des tests en fonction des exigences, et prend également en charge et crée de nouveaux tests en collaboration avec les développeurs.

"Cela ne peut pas durer éternellement et ne devrait pas se terminer tragiquement pour le processus de test!" - lorsqu'une telle idée a été visitée par des collègues, l'équipe du service après-vente du département de test est entrée dans le jeu. Le service après-vente a été confronté à la tâche: étudier Kotlin en peu de temps, afin que, si nécessaire, prendre en charge les tests rapidement.

Pour commencer


Le service après-vente a IntelliJ IDEA en service, et puisque Kotlin fonctionne au-dessus de la JVM et a été développé par JetBrains , il n'était pas nécessaire d'installer quoi que ce soit supplémentaire pour écrire du code.

Pour des raisons évidentes, nous allons sauter le processus d'apprentissage de la langue elle-même.

La première chose à faire a été de cloner le référentiel:
git clone https://gerrit.project.com/k/autotests

Ensuite, le projet a été ouvert et les paramètres de gradle ont été importés :



Pour une satisfaction et un confort absolus (* En fait, c'est un must), le plugin Spek a été installé:



Il a assuré le lancement des tests dans l'environnement de développement:



La première étape était terminée et il était temps de commencer à rédiger les tests eux-mêmes.

Les tests


Les bons gars du service après-vente n'appartiennent pas à tel ou tel produit. Ce sont ces employés qui sont pressés d'aider à configurer l'automatisation du projet, y compris à toutes les étapes du processus, puis à passer des tests aux testeurs de produits pour le support et la mise en service.

Et comme l'interaction des équipes internes du département de test est organisée de manière similaire, le département de service «demande» au moins les exigences de fonctionnalité à saisir.

Il peut sembler que c'est une impasse dans le cas de "K". Mais c'était là:

  • Un accès en lecture au référentiel où les sources du projet ont été stockées a été demandé;
  • Cloné le référentiel;
  • Ils ont commencé à plonger dans la fonctionnalité du produit en lisant des codes sources écrits en Java.

Qu'avez-vous lu?


Développement "K" a demandé d'écrire des tests de fonctionnalité, qui vous permet d'ajouter, de mettre à jour et de supprimer des produits à vendre. La mise en œuvre se composait de deux parties: «web» et «mobile».

Dans le cas du Web:

  • Pour ajouter des produits, une requête POST est utilisée, dont le corps contient du JSON avec des données.
  • Pour mettre à jour ou modifier des produits, une demande PUT est utilisée, dont le corps contient JSON avec les données modifiées.
  • Pour supprimer des marchandises, une demande DELETE est utilisée, dont le corps est vide.

Dans le cas du mobile:

Pour ajouter, mettre à jour et supprimer des produits, une requête POST est utilisée, dont le corps contient du JSON avec des données pour les opérations spécifiées.

C'est-à-dire JSON a trois nœuds:

  • "Ajouté": liste des produits ajoutés,
  • «Supprimé»: une liste des éléments supprimés,
  • «Mis à jour»: une liste de produits mis à jour.

Qu'avez-vous écrit?


Une classe de test contenant des spécifications de test a déjà été créée et contenait des méthodes de test (* un peu pas dans Spek), il était donc seulement nécessaire de l'étendre.

Pour le web

Test d'ajout de produit réussi:

  • Ajouter un élément
  • Vérifiez que l'élément a été ajouté.
  • Supprimer le produit créé (postcondition)

Code:

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

Test de retrait réussi des marchandises:

  • Ajouter un produit (condition préalable)
  • Nous supprimons les marchandises
  • Nous vérifions que le produit a bien été supprimé

Code:

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

Test négatif pour l'exécution de requêtes par un utilisateur non autorisé

  • Ajouter un élément
  • Vérifier l'état de la réponse
  • Une demande d'ajout d'un produit est envoyée sans en-tête d'autorisation. La réponse est accompagnée du statut 401 Non autorisé.

Code:

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

Pour mobile

Des fonctions d'aide ont été écrites pour obtenir les nœuds du corps de réponse et la formation du corps de demande.

Code:

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

Test d'ajout de produit réussi

  • Ajouter un élément
  • Vérifiez que l'élément a été ajouté.
  • Nous supprimons les marchandises (postcondition)

Code:

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

Test de mise à jour réussie du produit

  • Ajouter un produit (condition préalable)
  • Nous mettons à jour les marchandises
  • Vérifiez que l'élément ajouté a été mis à jour.
  • Nous supprimons les marchandises (postcondition)

Code:

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

Test de retrait réussi des marchandises:

  • Ajouter un produit (condition préalable)
  • Nous supprimons les marchandises
  • Nous vérifions que le produit ajouté est supprimé

Code:

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

Après avoir écrit les tests, il a fallu passer en revue le code.

Revue


Plus d'une douzaine de commits, beaucoup de correspondance avec les développeurs, la visite de forums, la communication avec Google. Et voici le résultat.

Code:

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

Résumé


Le code lui-même, les compétences linguistiques et la connaissance du framework sont loin d'être parfaits, mais le début n'est généralement pas mauvais.

Quand j'ai rencontré Kotlin, j'avais l'impression qu'il était du sucre syntaxique en Java. Et en écrivant le code avec toutes les fibres de l'âme, j'ai réussi à ressentir les mots: «entièrement compatible avec Java».

Spek, qui utilise des constructions de langage simples pour décrire les spécifications, fournit un pool complet de méthodes de test. C'est-à-dire donne ce qu'ils attendent de lui comme d'un framework de test.

Total - tous les tests en master. Tout s'est bien passé et le service après-vente sait désormais avec certitude qu'il pourra accompagner des collègues de K dans les moments difficiles.

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


All Articles