Histórico de teste do projeto K: Kotlin & Spek

Olá Habr!

Neste artigo, falaremos sobre o teste automático em um dos muitos projetos QIWI, com o codinome "K".



Quando organizamos os testes deste projeto, decidimos escolher o Kotlin prático e o hype, bem como Spek , que diz: "Você os chama de testes, nós os chamamos de especificações" (Você os chama de testes, os chamamos de especificações). Talvez essa abordagem seja adequada para você se tiver tarefas semelhantes.

Por que Kotlin e não outra coisa? Kotlin foi escolhido pelo desenvolvimento, para experimentar, já que esse produto em particular não era crítico e era possível praticá-lo ao vivo sem medo de que houvesse problemas.

A documentação oficial nos diz que "Spek está escrito em Kotlin, e as especificações que você escreve serão escritas em Kotlin" - isso responde muito claramente à pergunta: "Por que isso é necessário?"

Então ...

O que é e por que é necessário?


O projeto fornece ao seu parceiro um software, um aplicativo para Android. A maior parte dos testes recai sobre o back-end, portanto, nos concentraremos em testar a API REST.

Para um grupo que permite escrever testes e obter resultados, tudo fica claro: você precisa de uma linguagem de programação, estrutura de teste, cliente HTTP e relatórios. Mas e o ponto de entrada em nosso universo de teste?

Requisitos, são especificações, os desenvolvedores do projeto decidiram escrever na forma de testes. O resultado foi uma imagem interessante - BDD. Assim, Kotlin, Spek e khttp apareceram na arena.
O leitor atento perguntará - OK, mas onde estão os testadores?

Testadores


Depois de terminar dois coelhos com uma cajadada, o desenvolvimento deu ao testador de produtos requisitos e autotestes. Desde então, o testador expandiu a cobertura do teste de acordo com os requisitos e também oferece suporte e cria novos testes junto aos desenvolvedores.

"Isso não pode durar para sempre e não deve terminar tragicamente para o processo de teste!" - quando essa ideia foi visitada por colegas, a equipe do departamento de serviço do departamento de testes entrou no jogo. O departamento de serviço enfrentou a tarefa: estudar o Kotlin em um curto espaço de tempo, para que, se necessário, receba um suporte rápido dos testes.

Introdução


O departamento de serviço tem o IntelliJ IDEA em serviço e, como o Kotlin roda sobre a JVM e foi desenvolvido pela JetBrains , não foi necessário instalar nada extra para escrever código.

Por razões óbvias, pularemos o processo de aprendizado do próprio idioma.

A primeira coisa a começar foi clonar o repositório:
git clone https://gerrit.project.com/k/autotests

Em seguida, o projeto foi aberto e as configurações de gradle foram importadas :



Para total satisfação e conforto (* Na verdade, isso é obrigatório), o plug-in Spek foi instalado:



Ele garantiu o lançamento de testes no ambiente de desenvolvimento:



A primeira etapa foi concluída e chegou a hora de começar a escrever os testes.

Testes


Os mocinhos do departamento de serviço não pertencem a este ou aquele produto. Esses são os funcionários que estão com pressa para ajudar a configurar a automação no projeto, incluindo todas as etapas do processo, e depois passar os testes aos testadores de produtos para suporte e comissionamento.

E como a interação das equipes internas do departamento de testes é organizada de maneira semelhante, o departamento de serviço "solicita" pelo menos os requisitos para a entrada de recursos.

Pode parecer que este é um beco sem saída no caso de "K". Mas lá estava:

  • O acesso de leitura ao repositório onde as fontes do projeto foram armazenadas foi solicitado;
  • Clonou o repositório;
  • Eles começaram a mergulhar na funcionalidade do produto através da leitura de códigos-fonte escritos em Java.

O que você leu?


O desenvolvimento "K" pediu para escrever testes para o recurso, o que permite adicionar, atualizar e excluir produtos à venda. A implementação consistiu em duas partes: "web" e "mobile".

No caso da web:

  • Para adicionar produtos, uma solicitação POST é usada, cujo corpo contém JSON com dados.
  • Para atualizar ou editar produtos, é usada uma solicitação PUT, cujo corpo contém JSON com os dados alterados.
  • Para excluir mercadorias, uma solicitação DELETE é usada, cujo corpo está vazio.

No caso do celular:

Para adicionar, atualizar e excluir produtos, é usada uma solicitação POST, cujo corpo contém JSON com dados para as operações especificadas.

I.e. JSON tem três nós:

  • "Adicionado": lista de produtos adicionados,
  • "Removido": uma lista de itens removidos,
  • "Atualizado": uma lista de produtos atualizados.

O que você escreveu?


Uma classe de teste contendo especificações de teste já foi criada e continha métodos de teste (* um pouco fora do Spek); portanto, era necessário apenas expandi-la.

Para web

Teste para a adição bem sucedida de produtos:

  • Adicionar item
  • Verifique se o item foi adicionado.
  • Excluir o produto criado (pós-condição)

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

Teste para remoção bem-sucedida de mercadorias:

  • Adicionar produto (pré-condição)
  • Excluímos as mercadorias
  • Verificamos se o produto foi excluído

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

Teste negativo para execução de consultas por usuário não autorizado

  • Adicionar item
  • Verifique o status da resposta
  • Uma solicitação para adicionar um produto é enviada sem um cabeçalho de autorização. A resposta vem com o status 401 Não 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 celular

As funções auxiliares foram gravadas para obter os nós do corpo da resposta e a formação do corpo da solicitação.

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

Teste para a adição bem sucedida de produtos

  • Adicionar item
  • Verifique se o item foi adicionado.
  • Excluímos as mercadorias (pós-condição)

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

Teste para atualização bem-sucedida do produto

  • Adicionar produto (pré-condição)
  • Nós atualizamos os bens
  • Verifique se o item adicionado foi atualizado.
  • Excluímos as mercadorias (pós-condição)

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

Teste para remoção bem-sucedida de mercadorias:

  • Adicionar produto (pré-condição)
  • Excluímos as mercadorias
  • Verificamos que o produto adicionado foi excluído

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

Após escrever os testes, foi necessário fazer uma revisão do código.

Revisão


Mais de uma dúzia de confirmações, muita correspondência com desenvolvedores, visitando fóruns, se comunicando com o Google. E aqui está o 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 } } } }) 

Sumário


O código em si, as habilidades de linguagem e o conhecimento da estrutura estão longe de serem perfeitos, mas o começo geralmente não é ruim.

Quando conheci Kotlin, tive a sensação de que ele era açúcar sintático em Java. E enquanto escrevia o código com todas as fibras da alma, consegui sentir as palavras: "totalmente compatível com Java".

O Spek, que usa construções simples de linguagem para descrever especificações, fornece um conjunto completo de métodos de teste. I.e. dá o que eles querem dele a partir de uma estrutura de teste.

Total - todos os testes no mestre. Tudo deu certo, e o departamento de serviço agora sabe com certeza que será capaz de apoiar colegas da K em tempos difíceis.

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


All Articles