浅谈仓储UI自动化之路 | 京东物流技术团队

1 分层测试

分层测试:就是不同的时间段,不同的团队或团队使用不同的测试用例对产品不同的关注点进行测试。一个系统/产品我们最先看到的是UI层,也就是外观或者说整体,这些是最上层,最上层依赖下面的服务层,也就是接口或者模块,最底层就是单元,这个单元是函数或者方法。按照这三层选择不同时间段,不同团队不同测试用例进行的测试就是分层测试。

通读上述概念,先对分层测试有个大体的印象,下面结合测试金字塔模型来具体说明:

1.1 单元(Unit )测试

单元测试是针对代码单元(通常是类/方法)的测试,单元测试的价值在于能提供最快的反馈,在开发过程中就可以对逻辑单元进行验证。

1.2 接口(Service/服务/API)测试

接口测试是针对业务接口进行的测试,主要测试内部接口功能实现是否完整。如主要业务流是否能走通,异常处理是否正确,数据为空时校验等等。接口测试的主要价值在于接口定义相对稳定,不像界面或底层代码会经常发生变化,所以接口测试比较容易编写,用例的维护成本也相对较低。在接口层面准备测试的性价比相对较高。

1.3 集成(UI)测试

集成测试从用户的角度验证产品功能的正确性,测的是端到端的流程,并且加入用户场景和数据,验证整个业务流。集成测试的业务价值最高,它验证的是一个完整的流程,但因为需要验证完整流程,在环境部署、准备用例及实施等方面成本较高,实施起来并不容易。

1.4 分层测试总结

Google的自动化分层投入占比是:单元测试(Unit):占比70%;接口测试(Service):占比20%;集成测试(UI):占比10%。

测试过程中需要尽量提早介入测试,针对重点模块功能进行摸底测试,根据金字塔模型 越往上,越接近QA、业务和最终用户,发现问题后解决问题的成本会越高。采用分层测试存在以下优势:

  • 尽量测试前移,在开发前期发现问题解决问题,开发成本会迅速下降。
  • 不同时间段关注不同,分重点测试,层层防护。
  • 容易定位问题,测的哪一层,出现问题,就是哪一层的问题,很明确。
  • 分层测试在用例设计和执行测试的时候,更具有针对性,思维更加清晰,不容易遗漏。
  • 加强测试对代码实现的理解,可以更好的进行测试技能拓展。

最后,在具体实施时,层级如何划分要设计好,设计好对应层级的测试用例,且用例执行时要持续追踪,前面的工作要为后面的工作起到实际作用。

2 UI自动化

UI自动化测试,即通过模拟手动操作用户UI界面的方式,以代码方式实现自动操作和验证的一种自动化测试手段。从测试渠道上可以分为WebUI测试和App测试,WebUI包括PC和H5两个方向。

2.1 UI自动化作用

  • 重复性的功能测试及验证;
  • 避免疲惫操作时的人为测试遗漏;
  • 通过UI自动化操作获取其他测试数据的能力。

2.2 UI自动化优点

  • 用例编写简单,降低上手门槛;
  • 节省人工测试成本,提高功能测试、回归测试的测试效率;
  • 保障软件质量的一种手段和方式。

2.3 UI自动化缺点

  • UI控件的频繁变更导致控件定位;
  • 用例脚本的维护成本较高,投入和产出比例低;
  • 元素定位的不稳定导致用例的效率和稳定性差。

3 常见的UI自动化框架分析

常用的WebUI自动化测试工具主要有强大且免费开源的Selenium家族,也有体验良好收费很贵的QTP工具,还有新兴崛起的Cypress,以及其他工具。

3.1 Cypress和Selenium用户量对比

Cypress和Selenium的下载量对比分析:Selenium相对稳定,Cypress下载量在21年度正式超过了Selenium,并且分差不断拉大。

3.2 Cypress和Selenium实现架构对比

Selenium系统的架构:由代码通过JSON Wire网络协议和driver进行通信,由driver和真实浏览器交互操作,最终返回操作结果到代码。

Cypress系统的架构:使用 webpack 将测试代码中的所有模块 bundle 到一个 js 文件中。测试代码和被测程序在同一个浏览器的不同iframe 中,无需通过网络访问。

3.3 Cypress和Selenium环境框架对比

Cypress和Selenium自动化环境搭建对比:Cypress只需要安装的方式即可使用,Selenium作为库包形式提供,需要自己选择对应的框架,断言以及额外的依赖。

3.4 Cypress和Selenium环境对比汇总

4 如何做好UI自动化

4.1 我不想写UI自动化的N个理由

  • 自动化编写脚本成本高,selenium需要自己搭框架,cypress只能用js写,且都需要对前端有所涉略才能写好。
  • 自动化手动编写脚本案例需要很长时间,编写场景数过少,发现不了多少问题。
  • 录制、编写时候页面有变化时候每个用例都需要修改,需要专人维护,维护成本非常高。
  • 经常出现脚本问题出现的错误,页面加载异常等等,写出来的的用例非常不稳定。

4.2 我不得不写UI自动化的N个理由

  • 需要回归测试的场景太多,手工重复频繁执行太耗时。
  • 需要进行线上环境测试,无线上接口操作权限。
  • UI自动化测试更贴近用户实际使用场景,通过接口测试无法完全保障质量。

4.3 如何做好UI自动化---降低代码维护成本

针对不想写UI自动化又不得不做的时候,我们需要选择一个好的框架来管理我们的自动化用例。不管是selenium还是cypress,我们都需要将我们的自动化脚本代码尽量的复用。如果我们只是想测试流程数据的时候,需要将我们控件和操作进行封装。下面我们将主要用仓储Cypress自动化来举例子。

4.3.1 基础控件进行封装

一般系统的基础封装控件是有一定风格的,譬如下图中我们如何快速寻找到订单号的输入框呢?显然如果直接通过"请输入"字段查找会查到多个,无法定位唯一。针对仓储系统由于页面都是配置出来的,没有固定唯一的id管理。

通过dom树分析得出,普通的输入框可以通过label名称查找到对应的label,然后查找到公共的父节点el-form-item,再根据查找el-form-item的子节点el-form-item__content的子节点el-input来查找到要输入的输入框进行输入内容。

将此操作封装为两个基础自定义命令:

javascript 复制代码
//查找label所在的el-form-item控件组合根节点,正则全词匹配更加精确
Cypress.Commands.add('getElFormItemByLabel', (label) => {
  cy.get('.el-form-item__label').contains(new RegExp("^" + label + "$", "g")).first().parent('.el-form-item')
})


// 输入框填写值,根据传入的el-form-item找到el-input-inner对象进行输入,增加去除readonly事件,enter事件,以及强制输入
Cypress.Commands.add("cTypeWidthEvent", { prevSubject: 'element' }, ($elSelect, value, event = 'enter') => {
  cy.wrap($elSelect).find('.el-input__inner').then(($el)=>{
    $el.removeAttr('readonly')
    }).type(value + `{${event}}`,{force:true})
})


//后续使用输入框时候只需要这样使用
cy.getElFormItemByLabel('订单号').cTypeWidthEvent(orderNo)
cy.getElFormItemByLabel('派车单号').cTypeWidthEvent(TJNo)

根据dom树分析可以得到el-button class样式的span标签内容是设置,找到此唯一节点点击即可实现点击事件。

javascript 复制代码
//封装点击自定义命令
Cypress.Commands.add('btnClick', (label) => {
    cy.get('button > span').contains(new RegExp("^" + label + "$", "g")).parent('button').click()
})

//如下使用封装所有页面的点击事件
cy.btnClick('设置')

通过如上的方法首先将系统的基础操作元素都封装到customCommands中,作为系统基础控件管理。可以极大程度上降低维护控件变化的成本。

4.3.2 Page-Object模式针对系统页面进行管理

将如下的订单列表页面进行页面封装,譬如我在出库单据中心只会做根据订单号搜索,然后点击订单号跳转进入详情页。

只是需要建一个订单PO管理的页面class,由于仓储是用来跑流程的所以只封装一些用到的控件即可。

javascript 复制代码
//创建PO管理,通过封装的基础命令来封装控件操作,此最好做到只维护控件名称数据
export default class OrderCenterListPage{
    constructor() {}
    clearReceiveOrderDate(receiveStartOrderDate,receiveEndOrderDate){
        //删除接单时间
        cy.getElFormItemByLabel('接单时间').children('.el-form-item__content').find('.el-icon-clear').first().click({force:true})
    }

    orderNoFilterInput(orderNo){
        cy.getElFormItemByLabel('订单号').cTypeWidthEvent(orderNo)
    }

    openOrderDetail(orderNo){
        cy.get('.el-table_1_column_3').contains(orderNo).click()
    }
}

//同时将此页面的操作封装成一个自定义页面操作命令
// 按订单查询明细
Cypress.Commands.add('OrderCenterListPage.openOrderDetail', (orderNo) => {
    var page = new OrderCenterListPage();
    page.clearReceiveOrderDate()
    var routeName = 'queryOrderListInfo'+ Date.now()
    cy.intercept('POST','**/order/web/queryOrderListInfo').as(routeName)
    page.orderNoFilterInput(orderNo)
    cy.wait(`@${routeName}`).then((res=>{
        page.openOrderDetail(orderNo)
    }))
})

4.3.3 针对操作流程进行再度封装

上述封装如果针对页面测试的话已经够了,如果是要针对流程测试,类似仓储的出库流程中有如下的步骤,需要再次对每个页面操作进行封装到一个流程当中,进一步提升代码复用。

基于此,我们做了生产流程的封装,见如下图:

4.3.4 测试用例组织

首先新建一个测试套件,然后将每个页面的主要操作封装成流程的一个用例。为什么要这样做呢?将测试用例拆散验证方便失败重试,如果你写的在一个it里面意味着失败重试需要全量触发,反之只需要重试其中一个步骤即可,大大提升成功的效率和缩短重试执行的时间。还可以快速的发现问题所在的位置。

javascript 复制代码
import ExceptionInBoundFlow from '../../support/flow/exceptionInBoundFlow'
import passBackList from '../../fixtures/0_990/passback.json'

describe('5.0到6.0切仓出库全流程验证', () => {

  const flow = new ExceptionInBoundFlow()
  const ibOrderNo = 'UAT_'+Date.now()
  const oBOrderNo = 'WMSESL140760105630761'   //WMSESL140760105632249

  before(()=>{
    cy.clearCookies()
  })

  after(()=>{
    //cy.clearCookies()
  })

  beforeEach(() => {
    //登陆系统
    cy.intercept('**/*',(req) => {
      req.headers['origin'] = 'http://sunlon.wms.jdl.cn'
    }).as('headers')

    cy.visit(Cypress.env('baseUrl'))

  })

  it('1.查询生产流程和生产状态',  () => {
    flow.getOrderProductionInfo(oBOrderNo)
  })

  it('2.根据定位异常下单',  () => {
    flow.receiveIbOrder({oBOrderNo:oBOrderNo,ibOrderNo:ibOrderNo})
  })

  it('3.扫描收货',  () => {
    flow.scanReceiving({orderId:ibOrderNo,locationNo:Cypress.env('locationNo').pickLocation})
  })

  it('4.重新定位',  () => {
    flow.reLocate(oBOrderNo)
  })

  it('5.是否手工定位',  () => {
    flow.manualLocation(oBOrderNo)
  })

  it('6.任务分配',  () => {
    flow.createOutboundTask(oBOrderNo)
  })

  it('7.拣货',  () => {
    flow.pickNew(oBOrderNo,Cypress.env('locationNo').pickLocation)
  })

  it('8.前合流',  () => {
    flow.confluenceBeforeCheck()
  })

  it('9.复核',  () => {
    flow.check({platformNo:Cypress.env('review').defaultPlatformNo,containerNo:Cypress.env('review').defaultContainerNo, palletNo:null, defaultConsumableCode:Cypress.env('review').defaultConsumableCode})
  })

  it('10.后合流上架',  () => {
    flow.upToShipmentLocation(oBOrderNo,Cypress.env('locationNo').fahuoLocation)
  })

  it('11.客单生成包裹',  () => {
    flow.createPackage(oBOrderNo)
  })

  it('12.发货',  () => {
    flow.quickShip(oBOrderNo)
  })

  describe('13.校验生产单回传',  () => {
    for(const index in passBackList){
        const node = passBackList[index].node
        const whiteList = passBackList[index].whiteList
        const desc = passBackList[index].desc
        it(`校验生产单回传节点${index},订单号=${oBOrderNo},回传节点=${node},回传名称${desc},校验内容校验字段=${whiteList}`,  () => {
          cy.log(`校验生产单回传节点,订单号=${oBOrderNo},回传节点=${node},屏蔽校验字段=${whiteList}`).then(()=>{
            flow.passBackCompare(oBOrderNo, passBackList[index].node,passBackList[index].whiteList)
          })
        })
    }
  })
})

以上为UI自动化(实际上不限于UI)降低代码脚本维护成本的方法。

4.4 如何做好UI自动化---提升脚本效率以及稳定性

4.4.1 去掉等待

我们在编写脚本时候由于一些操作需要等待后台接口的返回才能进行下一步操作,我们可能会增加cy.wait(10000)设置等待时长10秒来处理。这种如果接口没有10秒内返回的话,会导致用例的失败。针对此我们采用了 cy.intercept来设置拦截接口路由,通过wait来等待后台接口返回后再进行下一步操作。

javascript 复制代码
   //设置路由名称
    var routeName = 'queryOrderListInfo'+ Date.now()
    cy.intercept('POST','**/order/web/queryOrderListInfo').as(routeName)
 //操作页面按钮触发请求
    page.orderNoFilterInput(orderNo)
    //等待页面请求结束后进行下一步操作
    cy.wait(`@${routeName}`).then((res=>{
        page.openOrderDetail(orderNo)
    }))

用 cy.intercept除了可以解决操作问题外,还可以用来进行判断断言接口返回值,并且根据接口返回值进行重试操作,增强稳定性。

scss 复制代码
cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo1')
    //点击查询
    page.search()
    cy.wait('@queryWaitTaskAssignOrderInfo1').then((res) => {
        // 针对响应进行断言
        if(res.response.body.resultValue.total != 1){
            console.log('查找待组单的订单不成功,可能是未定位完成,再等待一分钟')
            cy.wait(30000)
            cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo2')

            page.search()
            cy.wait('@queryWaitTaskAssignOrderInfo2').then((res) => {
                // 针对响应进行断言
                if(res.response.body.resultValue.total != 1){
                    console.log('查找待组单的订单不成功,可能是未定位完成,再等待一分钟')
                    cy.wait(60000)
                    cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo3')
                    page.search()
                    cy.wait('@queryWaitTaskAssignOrderInfo3')
                }
            })
        }
    })

最后还可以通过 cy.intercept修改请求属性,并且设置接口mock结果来解决外部接口依赖问题。

php 复制代码
//设置所有请求添加请求origin
cy.intercept('**/*',(req) => {
req.headers['origin'] = 'http://sunlon.wms.jdl.cn'
}).as('headers')

//将widgets接口mock掉
cy.intercept('POST', 'http://example.com/widgets', {
  statusCode: 200,
  body: 'it worked!'
})

4.4.2 数据传递

Cypress自动化通过上面4.3.4方式设计用例,提升稳定性问题,同时也带来了用例间如何传递数据的问题。在Selenium中同步传递数据使用一个全局变量可以解决,在Cypress中由于每个操作都是异步的,全局变量方法不可行。这里我们可以采用读写文件方式,读写cookie方式,配置文件方式读取静态变量。这里我们介绍一下cookie的方法(需要注意:cookie中是不支持中文的,如果有中文会系统异常报错)。

javascript 复制代码
//配置cookie全局生效:
Cypress.Cookies.defaults({
    preserve:/^testData*/
})
csharp 复制代码
//获取生产流程和生产状态写入cookie:
getOrderProductionInfo(oBOrderNo){
            var json = JSON.parse(res.resultValue[0].json)
            //获取单据类型映射
            this.#testData.shipmentOrderType=json.ruleDetail[0].value[0] 
            //获取生产流程
            this.#testData.productionInfo.location.mode=json.outboundProcessDto.locatingRuleVo.operationMode
            this.#testData.productionInfo.splitOrder.mode=json.outboundProcessDto.splitOrderRuleVo.operationMode
             //单据生产流程和状态存入缓存,注意缓存不能放汉字
             cy.setCookie('testData.info.productInfo',JSON.stringify(this.#testData))
            })
 })
javascript 复制代码
//cookie读取使用:
    receiveIbOrder({oBOrderNo,ibOrderNo,wait=30000}){
        //获取生产流程和上一步测试数据
        cy.getCookie('testData.info.productInfo').then(cookie=>{
             //存储sku和是否需要采购收货状态
             cy.setCookie('testData.info.productInfo',JSON.stringify(testData))
         })
    })

通过动态数据获取传递针对同一个订单不但实现了不同生产流程的通用执行操作,还实现了基于状态的重试,始得整个用例未成功的时候可以设置重新执行保证测试通过。

4.4.3 异步Promise获取方式

通过引入异步Promise的方式将代码中原先需要then层层递进的方法进行异步平铺返回结果。(需要注意的是不能在测试用例中将it改成async异步属性)

javascript 复制代码
 export async function doTaskAsign({orderNo,orderType,pickType}){
    var batchNo = await promisify(cy['assembleFormCreat.doTaskAssign']())
    return batchNo
}

4.4.4 屏蔽系统异常提升稳定性

通过在support中引入如下配置解决系统错误报错,非cypress断言报错引起的失败现象。

javascript 复制代码
Cypress.on('uncaught:exception', (err, runnable) => {
    // returning false here prevents Cypress from
    // failing the test
    return false
})

4.4.5 使用录制来辅助定位复杂的控件

作为初级使用者,可能通过录制能更好更快的学习语法,Cypress也支持录制的方式。

只需要配置: "experimentalStudio": true

4.4.6 漂亮的工程结构框架

5 学习UI自动化的途径

Selenium本文没做介绍,比较成熟的框架网上也有一堆项目案例,各个公司也有类似开发的录制工具。Cypress可以学习小菠萝测试笔记101篇总结,该位大佬基本上介绍了Cypress所有的API使用实践,测试组这边也有一本实体书可以供参考学习,遇到问题还可以去Cypress社区群求助。

其他就不多说了,欢迎大家一起来学习Cypress自动化,为我们的自动化事业添砖加瓦

作者:京东物流 徐桂贵

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

相关推荐
安冬的码畜日常5 天前
【玩转 Postman 接口测试与开发2_007】第六章:Postman 测试脚本的创建(下):预请求脚本及环境变量在多个请求自动运行中的应用
测试工具·postman·测试·runner·api测试·自动测试
北京_宏哥11 天前
《最新出炉》系列入门篇-Python+Playwright自动化测试-50-滚动条操作
python·前端框架·测试
kida_yuan13 天前
【从零开始】6. RAG 应用性能压测工具(番外篇)
后端·llm·测试
孤蓬&听雨18 天前
Kafka自动生产消息软件(自动化测试Kafka)
分布式·kafka·自动化·测试·生产者
帅儿二郎20 天前
ELK:日志监控平台部署-基于elastic stack 8版本
linux·运维·elk·自动化运维·elastic·日志监控平台·日志分析平台
帅得不敢出门21 天前
Python+Appium+Pytest+Allure自动化测试框架-安装篇
python·appium·自动化·pytest·测试·allure
陈明勇22 天前
自动化测试在 Go 开源库中的应用与实践
后端·go·测试
帅得不敢出门23 天前
Python+Appium+Pytest+Allure自动化测试框架-代码篇
python·appium·自动化·pytest·测试·allure
Dylanioucn23 天前
《解锁 TDD 魔法:高效软件开发的利器》
后端·功能测试·测试·测试驱动开发·tdd
北京_宏哥24 天前
《最新出炉》系列入门篇-Python+Playwright自动化测试-41-录制视频
前端·python·测试