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

京东云开发者
• 阅读 268

1 分层测试

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

浅谈仓储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,并且分差不断拉大。

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

3.2 Cypress和Selenium实现架构对比

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

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

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

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

3.3 Cypress和Selenium环境框架对比

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

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

3.4 Cypress和Selenium环境对比汇总

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

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管理。

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

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

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

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

//查找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)

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

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

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

//封装点击自定义命令
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模式针对系统页面进行管理

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

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

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

//创建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 针对操作流程进行再度封装

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

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

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

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

4.3.4 测试用例组织

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

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来等待后台接口返回后再进行下一步操作。

   //设置路由名称
    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除了可以解决操作问题外,还可以用来进行判断断言接口返回值,并且根据接口返回值进行重试操作,增强稳定性。

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结果来解决外部接口依赖问题。

//设置所有请求添加请求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中是不支持中文的,如果有中文会系统异常报错)。

//配置cookie全局生效:
Cypress.Cookies.defaults({
    preserve:/^testData*/
})
//获取生产流程和生产状态写入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))
            })
 })
//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异步属性)

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

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

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

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

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 漂亮的工程结构框架

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

5 学习UI自动化的途径

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

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

作者:京东物流 徐桂贵

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

点赞
收藏
评论区
推荐文章
浅谈测试用例设计 | 京东云技术团队
一个良好的测试用例除了可以帮助测试人员阅读,理解,修改之外,也要方便我们去管理它,从而提高测试工作的质量和效率。不同的业务条线或者团队可以根据自己需要制定一些规则,让大家在进行测试用例设计遵守。
Stella981 Stella981
3年前
Mock工具之Mockito实战
在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Moc
可莉 可莉
3年前
19.unittest原理
单元测试单元测试(unittesting)是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。unittest运行原理
Wesley13 Wesley13
3年前
C#单元测试
什么叫单元测试(unittesting)?是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软
Wesley13 Wesley13
3年前
Java单元测试总结
  单元测试的重要性这里就不说了,直接进入正题。很多程序员不喜欢写单元测试,导致项目经常会花很多时间去debug,这完全得不偿失。对关键方法进行单元测试,可以在早期业务逻辑还没那么复杂的时候,尽快排除症结。  在dao,manager,server,web这样的分层项目中,通常单元测试是要写在server层的,因为server层可以测的最多。本文中不介绍
Stella981 Stella981
3年前
SpringBoot(20)
  我们在写单元测试的时候,除了接口直接抛异常而导致该单元测试失败外,还有种是业务上的错误也代表着该单元测试失败。好比我们在测试接口的时候,  该接口返回是1代表成功,如果是0那就代表是失败的,这个时候可以考虑使用断言。  一、原理我们知道,我们可以通过断言来校验测试用例的返回值和实际期望值进行比较,以此来判断测试是否通过。那我们先来看下如果失败的情
可莉 可莉
3年前
2020DevOps状态报告——平台模型:扩展DevOps的新方法
平台模型是我们在这个领域看到越来越多的方法,它源于负责产品或服务的端到端交付的产品团队的理念。如果只应用于单一的产品,或者几个产品,它的效果很好。但如果有数百种产品或服务,把一个产品团队用于这些产品,对每一个来说都是低效和昂贵的。想象10个团队,每个团队都有自己的技术栈、工具链和流程。会一直重复解决类似的问题、花太多的时间来评估技
Stella981 Stella981
3年前
2020DevOps状态报告——平台模型:扩展DevOps的新方法
平台模型是我们在这个领域看到越来越多的方法,它源于负责产品或服务的端到端交付的产品团队的理念。如果只应用于单一的产品,或者几个产品,它的效果很好。但如果有数百种产品或服务,把一个产品团队用于这些产品,对每一个来说都是低效和昂贵的。想象10个团队,每个团队都有自己的技术栈、工具链和流程。会一直重复解决类似的问题、花太多的时间来评估技
Java单元测试及常用语句 | 京东物流技术团队
编写Java单元测试用例,即把一段复杂的代码拆解成一系列简单的单元测试用例,并且无需启动服务,在短时间内测试代码中的处理逻辑。写好Java单元测试用例,其实就是把“复杂问题简单化,建单问题深入化“。在编写的过程中,我们也可以对自己的代码进行一个二次检查。
京东云开发者 京东云开发者
5个月前
研发视角浅谈R2流量回放测试
一、背景测试小伙伴们在2023年保障了团队线上系统0问题,这简直就是一项了不起的壮举!这得益于咱们测试组同事对工作的细致投入、风险把控、以及严格遵循流程规范进行测试用例评审、自动化建设、联调推动、回归验证、常态化压测、大促高保真压测、引流回放等多重保险策略