K项目的测试历史:Kotlin&Spek

哈Ha!

在本文中,我们将讨论代号为“ K”的众多QIWI项目之一的自动测试。



当我们组织该项目的测试时,我们决定选择实用的炒作Kotlin以及Spek ,后者表示“您称其为测试,我们称其为规格”(您称其为测试,而称其为规格)。 如果您面临类似的任务,这种方法也许适合您。

为什么选择科特林而不是其他? 开发人员选择Kotlin进行试验,因为该特定产品并不重要,并且可以在不担心会出现问题的情况下进行实际试用。

官方文档告诉我们“ Spek用Kotlin编写,而您编写的规范将用Kotlin编写”-这很清楚地回答了以下问题:“为什么需要这样做?”

所以...

它是什么,为什么需要它?


该项目为其合作伙伴提供了软件,该软件是Android的应用程序。 大多数测试都在后端,因此我们将重点测试REST API。

对于允许您编写测试并获得结果的一堆东西,一切都很清楚:您需要一种编程语言,测试框架,HTTP客户端和报告。 但是,进入我们的测试领域的切入点呢?

需求,它们是规范,是项目开发人员决定以测试的形式编写的。 结果是一张有趣的图片-BDD。 因此,Kotlin,Spek和khttp出现在舞台上。
细心的读者会问-好吧,但是测试人员在哪里?

测试人员


用一块石头完成了两只鸟,这项开发为产品测试人员提供了要求和自动测试功能。 从那时起,测试人员根据要求扩展了测试范围,并与开发人员一起支持并创建了新的测试。

“这不可能永远持续下去,并且不应悲惨地结束测试过程!” -当同事们提出这样的想法时,测试部门服务部门的团队进入了比赛。 服务部门面临的任务是:在短时间内学习Kotlin,以便在必要时采取快速的测试支持。

开始使用


服务部门提供了IntelliJ IDEA服务,并且由于Kotlin在JVM之上运行并且是由JetBrains开发的,因此无需额外安装任何东西来编写代码。

由于明显的原因,我们将跳过学习语言本身的过程。

首先要克隆存储库:
git clone https://gerrit.project.com/k/autotests

然后打开项目并导入gradle设置:



为了获得完全的满意和舒适(*实际上,这是必须的), 安装Spek 插件



他确保在开发环境中启动测试:



第一阶段已经完成,是时候开始自己编写测试了。

测验


服务部门的好人不属于该产品。 这些员工急于帮助在项目上(包括流程的所有阶段)设置自动化,然后将测试传递给产品测试人员以寻求支持和调试。

并且由于测试部门内部团队的交互以类似的方式组织,因此服务部门至少“询问”了输入功能的要求。

在“ K”的情况下,这似乎是一个死胡同。 但这是:

  • 要求对存储项目源的存储库具有读取权限;
  • 克隆存储库;
  • 他们开始通过阅读用Java编写的源代码来投入产品的功能。

你读了什么?


开发“ K”要求编写功能测试,以便您添加,更新和删除要销售的产品。 该实现包括两个部分:“网络”和“移动”。

对于网络:

  • 要添加产品,将使用POST请求,该请求的正文包含带有数据的JSON。
  • 为了更新或编辑产品,使用了一个PUT请求,该请求的主体包含带有更改后的数据的JSON。
  • 要删除货物,将使用主体为空的DELETE请求。

对于手机:

要添加,更新和删除产品,将使用POST请求,该请求的正文包含JSON和指定操作的数据。

即 JSON具有三个节点:

  • “已添加”:已添加产品的列表,
  • “已删除”:已删除项目的列表,
  • “已更新”:已更新产品的列表。

你写了什么


已经创建了一个包含测试规范的测试类,并且包含测试方法(*在Spek中没有一点),因此仅需扩展它即可。

对于网络

测试是否成功添加了产品:

  • 新增项目
  • 检查是否已添加项目。
  • 删除创建的产品(后置条件)

代码:

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

对未经授权的用户执行查询的负面测试

  • 新增项目
  • 检查响应状态
  • 发送添加产品的请求而没有授权标头。 答案带有401未经授权状态。

代码:

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

对于手机

编写了辅助函数,以从响应主体和请求主体的构成中获取节点。

代码:

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

测试产品成功添加

  • 新增项目
  • 检查是否已添加项目。
  • 我们删除货物(后置条件)

代码:

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

编写测试后,有必要对代码进行审查。

复习


十几次提交,与开发人员的大量通信,访问论坛,与Google交流。 这就是结果。

代码:

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

总结


代码本身,语言技能和框架知识都不是完美的,但是开始通常还不错。

当我遇到Kotlin时,我感到他是Java中的语法糖。 当我用尽全力编写代码时,我设法感觉到这样的词:“与Java完全兼容”。

Spek使用简单的语言结构描述规范,提供了完整的测试方法库。 即 从测试框架中给出他们想要的东西。

总计-主测试中的所有测试。 一切都解决了,服务部门现在确定可以在困难时期为K的同事提供支持。

Source: https://habr.com/ru/post/zh-CN419699/


All Articles