
分享一个针对金蝶云星空做的WebUI自动化测试项目,只支持HTML5界面,希望抛砖引玉获得一些金蝶云星空WebUI自动化测试或CI/CD建设方面的讨论与建议。
# Nubula自动化测试项目介绍
```
_ __ __ __
/ | / /__ / /_ __ __/ /___
/ |/ / _ \/ __ \/ / / / / __ \
/ /| / __/ /_/ / /_/ / / /_/ /
/_/ |_/\___/_.___/\__,_/_/\__,__\
```
## 概述
- Nebula针对金蝶云星空(K3Cloud)开发的Web UI自动化测试框架。
- Nebula提供操作K3Cloud的的各类接口(如过滤单据,打开单据,字段取值/赋值,点击按钮等)。
- Nebula是基于Selenide和少量Selenium接口并结合K3Cloud使用习惯进行二次封装,旨在简化接口,方便调用。
- Nebula内置了多种动态等待、重试机制,用例运行稳定性大幅提高。
## 架构介绍
### 技术栈
Java + Spring + Selenide + TestNg + Allure + Maven
- 使用Java语言
- 使用Spring依赖注入特性,JDBC模板等简化代码
- Selenide是一个使用Java语言基于Selenium开发的Web自动化框架,它提供了对html元素的操作接口
- TestNg是Java优秀的单元测试框架,它提供测试用例组织,执行,正确性断言等测试相关功能
- Allure是一个图文并茂的优秀测试报告框架,结合TestNg可直观展示测试用例执行结果及分析。
- 使用Maven进行依赖管理,包管理。
### 项目结构

### 架构图

#### 模块介绍
- actions:对Selenide进行二次封装,是页面元素操作的接口(如点击、键盘输入、下拉列表选择、复选框勾选、获取元素value,动态等待元素出现/消失等)。部分Selenide无法实现的功能直接调用Selenium接口或执行JS代码实现,扩展了Selenide接口功能。
- pagecomponents:使用页面组件模型概念进行设计,不同于PageObjectModel(页面对象模型)将一个页面封装为一个对象的模式,页面组件模型将K3CLOUD系统界面拆分为多种组件,一个页面将由多个组件组合而成(如一个简单的单据页面对象可以由单据头组件对象,单据体组件对象组成)。
- pagemodel:页面模版对页面组件进行组合,封装了典型的页面对象。页面对象为测试用例提供操作特定页面的能力。
- businessflow:将某些常用、固定的业务流程功能进行封装,businessflow允许跨页面操作,是多个pagemodel对象的组合。
- testdata:存放测试所需的Sql语句对象,Excel数据文件等。
- service:用于定义各类服务功能如系统登录。
- dataprovider:定义自定义注解为测试数据提供方法指明测试数据位置;定义测试数据处理方法,解析Excel文档数据,并返回迭代器对象供TestNg调用。
- listener:定义测试执行结果处理监听器,实现TestNg用例重试接口。
- common:定义框架所需Enum类型、异常类型、参数类型,字段类型等。
- utils:定义各种工具类供支撑层、服务层,业务层使用,如日期处理工具类,JDBC连接池工具类,自定义Java工具类等。
- logs:存放测试过程日志。
### 操作浏览器原理
#### Selenide
Nebula基于Selenide框架开发,Selenide是一个由Java语言开发,基于Selenium2.0(又称Selenium WebDriver)的Web自动化框架。Selenide简化了Selenium接口操作,使用更简单的配置,支持Ajax,封装了对异常的处理。代码可维护性,稳定性较Selenium有明显提高。
引用Selenide官网介绍:
> WHAT IS SELENIDE?
> Selenide is a framework for test automation powered by Selenium WebDriver that brings the following advantages:
Concise fluent API for tests Ajax support for stable tests Powerful selectors Simple configuration
You don't need to think how to shutdown browser, handle timeouts and StaleElement Exceptions or search for relevant log lines, debugging your tests.
Just focus on your business logic and let Selenide do the rest
#### Selenium WebDriver
Selenium 2.0,又称 Selenium WebDriver,它利用的原理是:使用浏览器原生的 WebDriver 实现页面操作。

1. 当使用 Selenium2.0 启动浏览器 Web Browser 时,后台会同时启动基于 WebDriver Wire 协议的 Web Service 作为 Selenium 的 Remote Server,并将其与浏览器绑定。绑定完成后,Remote Server 就开始监听 Client 端的操作请求。
2. 执行测试时,测试用例会作为 Client 端,将需要执行的页面操作请求以 Http Request 的方式发送给 Remote Server。该 HTTP Request 的 body,是以 WebDriver Wire 协议规定的 JSON 格式来描述需要浏览器执行的具体操作。
3. Remote Server 接收到请求后,会对请求进行解析,并将解析结果发给 WebDriver,由 WebDriver 实际执行浏览器的操作。
4. WebDriver 可以看做是直接操作浏览器的原生组件(Native Component),所以搭建测试环境时,通常都需要先下载浏览器对应的 WebDriver。
### PageComponentModel vs PageObjectModel
#### PageObjectModel页面对象模型
页面对象模型的核心理念是,以页面(Web Page 或者 Native App Page)为单位来封装页面上的控件以及控件的部分操作。而测试用例,更确切地说是操作函数,基于页面封装对象来完成具体的界面操作。

PageObjectModel页面对象模型适合对每个Web页面相对独立、功能明确的产品进行自动化设计。K3CLOUD每个功能页面以单据形式呈现,系统内包含大量单据(页面),每个单据结构相似,单据内元素操作方式相同,若使用页面对象模型以单据为单位封装页面控件及操作,将存在大量重复代码,同时几百上千个页面对象也极不易使用。
#### PageComponentModel页面组件模型
为解决此痛点,Nebula使用PageComponentModel页面组件模型的方式,将K3CLOUD系统页面根据功能及元素定位的相似关系分割为多个组件,单据(页面)由组件组成,大幅提高代码复用率,典型的调用方式为XXXPageModel.YYYComponent.ZZZOperation()

如“接单目标设定”单据,此单据由一个单页签单据头(标准单据头包含多个页签,单页签单据头内部元素html代码层级与标准单据头不同,无法通用,故单页签单据头单独作为一个组件)组件,一个标准单据体组件组成,其代码组织形式如下。
> HeadOnlyOneTabCompImpl.java
```java
@Component("headOnlyOneTabCompImpl")
public class HeadOnlyOneTabCompImpl extends BaseHeadCompImpl implements IHeadOnlyOneTabComp {
public HeadOnlyOneTabCompImpl(){
super.textField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]/descendant::input[1]";
super.baseField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]/descendant::input[1]";
super.selectField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]";
super.isSelectField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class = 'k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]//span[@class = 'k-icon k-i-arrow-s']";
super.isBaseField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class = 'k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]//span[@class = 'k-icon k-i-search']";
super.textValueInput_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//span[translate(text(), ' ', '') = '%s']/parent::div/following-sibling::div[1]/descendant::span[@class = 'ui-poplistedit-displayname']";
super.baseValue_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class = 'k-content k-state-active']//span[@role='kdspanlabel' and @title='%s']/parent::div/following-sibling::div[@class='kdItemContainer_editorct']//span[@class='ui-poplistedit-displayname']";
super.selectValue_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class = 'k-content k-state-active']//span[@role='kdspanlabel' and translate(@title, ' ', '') = '%s']/parent::div/following-sibling::div[@class='kdItemContainer_editorct']//span[@class='k-input']";
}
@Resource
IActions actions;
@Override
/**
* 单据头文本字段赋值
* */
public void setText(String fieldName, String value) {
actions.clickByJs(String.format(textField_ArgsLoc, fieldName));
actions.input(String.format(textField_ArgsLoc, fieldName), value, false,true);
clickBlank();
}
}
```
**HeadOnlyOneTabCompImpl**类,继承自**BaseHeadCompImpl**,**BaseHeadCompImpl**类中定义了通用单据头元素定位语句,单据头常用操作方法,单页签单据头与通用单据头仅在元素定语句,文本字段赋值方法上存在差异,故**HeadOnlyOneTabCompImpl**类仅需修改基类元素定位语句、重写文本字段赋值方法,其他元素操作方法直接调用基类方法,即可实现单页签单据头所有操作功能。
>UniversalBodyCompImpl.java
```java
@Component("universalBodyCompImpl")
public class UniversalBodyCompImpl extends BaseBodyCompImpl implements IUniversalBodyComp {
String otherBaseField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//*[@data-field=//th[@data-title='%s']/@data-field and @data-rowid='%d']/ancestor::td/descendant::input[2]";
String checkBoxField_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'TAB') and @class='k-content k-state-active']//*[@data-field=//th[@data-title='%s']/@data-field and @data-rowid='%d']/label";
String textFieldInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'ui-poplistedit-displayname']/preceding-sibling::input";
String baseFieldInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'ui-poplistedit-displayname']/preceding-sibling::input";
String selectFieldInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'k-dropdown-wrap k-state-default']";
String textValueInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'ui-poplistedit-displayname']";
String baseValueInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'ui-poplistedit-displayname']";
String selectValueInBodyHead_ArgsLoc = "//div[contains(@id, '-FMAINTAB_c-') and @class='k-content k-state-active']//div[contains(@id, 'SPLITECONTAINER') and @splitter='two']//div[contains(@id, '-FTAB') and @class='k-content k-state-active']//span[text()= '%s']/../following-sibling::div[1]//span[@class = 'k-dropdown-wrap k-state-default']/span[1]";
@Resource
IActions actions;
@Override
public void setBaseOther(String fieldName, int rowNum, String value) {
return;
}
@Override
public void checkCheckBox(String fieldName, int rowNum) {
actions.clickByJs(String.format(checkBoxField_ArgsLoc, fieldName, rowNum-1));
}
@Override
public void setTextInBodyHead(String fieldName, String value) {
actions.input(String.format(textFieldInBodyHead_ArgsLoc, fieldName), value,true, true);
clickBlank();
}
@Override
public void setSelectInBodyHead(String fieldName, String value) {
actions.selectByContent(String.format(selectFieldInBodyHead_ArgsLoc, fieldName), String.format(selectItem_ArgsLoc, value));
}
@Override
public void setBaseInBodyHead(String fieldName, String value) {
actions.clickByJs(String.format(baseFieldInBodyHead_ArgsLoc, fieldName));
actions.input(String.format(baseFieldInBodyHead_ArgsLoc, fieldName), value,false, true);
actions.sleepBaby(1);
//actions.click(String.format(baseItem_ArgsLoc, value));
actions.clickByJs(String.format(baseItem_ArgsLoc, value));
}
@Override
public String getTextInBodyHead(String fieldName) {
String value = actions.getValue(String.format(textValueInBodyHead_ArgsLoc, fieldName));
if (value == null){
return actions.getText(String.format(textValueInBodyHead_ArgsLoc, fieldName));
}
return value;
}
@Override
public String getBaseInBodyHead(String fieldName) {
String value = actions.getValue(String.format(baseValueInBodyHead_ArgsLoc, fieldName));
if (value == null){
return actions.getText(String.format(baseValueInBodyHead_ArgsLoc, fieldName));
}
return value;
}
@Override
public String getSelectInBodyHead(String fieldName) {
String value = actions.getValue(String.format(selectValueInBodyHead_ArgsLoc, fieldName));
if (value == null){
return actions.getText(String.format(selectValueInBodyHead_ArgsLoc, fieldName));
}
return value;
}
}
```
与单页签单据头不同**UniversalBodyCompImpl**通用单据体类,对基类**BaseBodyCompImpl**进行了扩展,添加了一些更为少见的单据体元素及相关操作(如勾选复选框,操作位于单据体中以单据头形式展示的字段),代码开头部分为新加入元素的定位语句,后续方法则是这些元素操作方法的具体定义。
>OrderTargetSettingBill.java
```java
package com.luthai.autotest.ui.pagemodel;
import com.luthai.autotest.ui.pagecomponents.IHeadOnlyOneTabComp;
import com.luthai.autotest.ui.pagecomponents.IUniversalBodyComp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 接单目标设定单据
* */
@Component
public class OrderTargetSettingBill {
@Autowired
public IHeadOnlyOneTabComp billhead;
@Autowired
public IUniversalBodyComp billBody;
}
```
**OrderTargetSettingBill**类作为单据模型,通过组合**IHeadOnlyOneTabComp**接口实现类对象与**IUniversalBodyComp**接口实现类对象,同时具有操作单页签单据头与通用单据体能力,测试用例调用**OrderTargetSettingBill**单据模型对象内部组件的方法,实现操作单据的具体业务功能。
### 等待机制
稳定性是GUI自动化测试是否有效最重要的指标,GUI自动化测试不稳定因素主要有:
- 页面控件属性的细微变化
- 随机页面延迟造成控件识别失败
- 非预计弹出框
- 测试数据问题
其中最频繁发生并较难处理的是“随机页面延迟造成控件识别失败”,以下介绍Nebula解决“随机页面延迟造成控件识别失败”问题使用的几种方法。
#### 动态等待
Selenide自带元素的动态等待定位,根据设置的timeout时间及判断条件,Selenide使用用户给出的元素定位语句(CSS或Xpath形式的元素定位语句,Nebula使用的是Xpath)在timeout时间内,每0.5秒轮询DOM一次,查找元素。
Selenide动态等待
```java
$x(eleLoc).shouldBe(Condition.exist);
```
设置定位超时时间
```java
import com.codeborne.selenide.Configuration;
public BaseTestCase(){
Configuration.timeout = 4000;
}
```
#### 定位等待
定义判断元素是否存在方法**isElementExist()** 使用Selenide动态等待方法shouldBe(),并加入重试机制,进行元素操作时首先使用**isElementExist()** 方法判断元素是否存在,存在则定位元素并进行操作,不存在时抛出ElementNotFoundException异常。
```java
public boolean isElementExist(String eleLoc) {
for (int i = 0; i < 3; i++) {
try {
$x(eleLoc).shouldBe(Condition.exist);
return true;
} catch (Error error) {
if (i < 2) {
log.debug(String.format("未找到元素%s,进行第%d次重试...", eleLoc, i + 1));
}
}
}
return false;
}
```
```java
@Override
/**
* 元素赋值
* 元素定位默认4s动态等待时间并最多重试2次,重试2次仍未定位到元素抛出com.luthai.autotest.ui.common.ElementNotFoundException异常
* @eleLoc: xpath元素定位语句
* */
public void input(String eleLoc, String content) {
if (isElementExist(eleLoc)) {
SelenideElement element = $x(eleLoc);
element.sendKeys(content);
} else throw new ElementNotFoundException(String.format("错误:重试2次仍未找到元素%s", eleLoc));
}
```
#### 参照物等待
“定位等待”是一种被动等待方式,是用户在定位元素时被动的无感知的应用的一种等待方式,参照物等待则是一种主动的等待方式,需要用户主动调用相关等待方法使程序在某一位置暂停执行并轮询判断参照物的状态从而实现动态等待。
如下图的业务场景,批量下推时弹出“正在批量下推”进度窗体,此时代码需等待下推完成后进行下一步操作。此种类型等待时间因数据量而异,可通过判断进度窗体是否消失决定代码是否继续执行。

```java
@Component
public class Wait implements IWaitElementDisplay {
private final static String pushProgress = "//span[@class = 'k-window-title' and text()='正在批量下推']";
@Autowired
IActions actions;
@Override
public void waitPushProgress(int timeoutSecond) {
actions.waitElementDisappear(pushProgress, timeoutSecond);
actions.sleepBaby(3);
}
}
```
**Wait**类封装各种参照物等待方法,**String pushProgress**定义批量下推进度窗体的定位语句,**waitPushProgress()** 方法接收一个timeout时间作为参数,超时时间内每0.5秒轮询一次判断元素是否消失,通常参照物等待超时时间应设置为比根据经验所得业务执行时间略大。
#### Ajax请求等待
点击按钮,加载数据此类操作时,页面会向服务器发送Ajax请求,此时页面会出现一个加载图片占据html最顶层,此时对元素的任何操作都无法进行,Ajax请求结束后(收到响应)加载图片消失,元素恢复可操作状态。

Nebula通过以下方式处理Ajax请求等待:
1. 登录系统后在系统主界面顶部添加ajax状态提示栏,展示ajax请求状态及数量
2. 登录系统后执行JS代码,监听ajax请求状态,并将状态及数量信息显示在提示栏
3. 定义wait_ajax()方法,ajax状态满足一定条件时跳出等待,否则继续等待。
4. 在点击类操作方法中调用wait_ajax()方法,进行Ajax请求等待。
**1. 登录系统后在系统主界面顶部添加ajax状态提示栏,展示ajax请求状态及数量**
```java
public void addAjaxMonitor() {
String add_screen_jq = "$(document).ready(function(){$(\"div[id$='-FSEARCH']\").before(\"
\");})";
executeJs(add_screen_jq);
}
```

**2. 登录系统后执行JS代码,监听ajax请求状态,并将状态及数量信息显示在提示栏**
```java
@Override
public void addAjaxMonitor() {
String add_screen_jq = "$(document).ready(function(){$(\"div[id$='-FSEARCH']\").before(\"
\");})";
executeJs(add_screen_jq);
String jq = "var ajax_count = 0;\n" +
" $(document).ready(function(){\n" +
"\n" +
" \t\t\t$(document).ajaxStart(function(){\n" +
" \t\t\t\t$(\"#AT_state_v\").html(\"ajaxStart!\")\n" +
" \t\t\t\tconsole.log(\"strat!\");\n" +
" \t\t\t\tajax_count +=1;\n" +
" \t\t\t\t$(\"#AT_count_v\").html(ajax_count)\n" +
" \t\t\t});\n" +
" \t\t\t$(document).ajaxStop(function(){\n" +
" \t\t\t $(\"#AT_state_v\").html(\"ajaxStop!\")\n" +
" \t\t\t console.log(\"stop!\");\n" +
"\n" +
" \t\t\t});\n" +
" \t\t\t})";
executeJs(jq);
}
```


**3. 定义wait_ajax()方法,ajax状态满足一定条件时跳出等待,否则继续等待。**
```java
public void wait_ajax() {
int count = 0;
while (count < 3){
String stateValue = getText("//div[@id = 'AT_state_v']");
int countValue = Integer.parseInt(getText("//div[@id = 'AT_count_v']"));
if (stateValue.equals("ajaxStart!")){
count = 0;
sleepBaby(0.5);
continue;
}
sleepBaby(0.5);
if(getText("//div[@id = 'AT_state_v']").equals("ajaxStart!") || countValue != Integer.parseInt(getText("//div[@id = 'AT_count_v']"))){
count = 0;
continue;
}
count++;
}
}
```
获取ajax状态提示栏的状态和数量信息,每0.5秒轮询一次,如果是“ajaxStart!”状态则继续等待,当状态为“ajaxStop!”时等待0.5秒重新获取ajax状态提示栏的状态和数量信息,若连续3次0.5秒内没有新的ajax请求产生则停止等待。
**4. 在点击类操作方法中调用wait_ajax()方法,进行Ajax请求等待。**
```java
@Override
/**
* 元素点击方法
* 元素定位默认4s动态等待时间并最多重试2次(最多等待4*(1+2)=12s),重试2次仍未定位到元素抛出com.luthai.autotest.ui.common.ElementNotFoundException异常
* @eleLoc: xpath元素定位语句
* */
public void click(String eleLoc) {
if (isElementExist(eleLoc)) {
SelenideElement element = $x(eleLoc);
element.click();
wait_ajax();
} else throw new ElementNotFoundException(String.format("错误:重试2次仍未找到元素%s", eleLoc));
}
```
### 重试机制
重试机制是提高测试稳定性、准确性的最后保障,其主要作用是在定位、操作,用例执行异常时发起重试,降低因系统卡顿,网络波动等异常情况对测试稳定性、准确性带来的影响。Nebula重试机制根据其应用位置主要分为定位重试、操作重试、用例重试。
#### 定位重试
定位重试是指在元素定位时若未定位到元素则重新进行定位直到到达重试次数上限(当前设置为重试2次)
```java
public boolean isElementExist(String eleLoc) {
for (int i = 0; i < 3; i++) {
try {
$x(eleLoc).shouldBe(Condition.exist);
return true;
} catch (Error error) {
if (i < 2) {
log.debug(String.format("未找到元素%s,进行第%d次重试...", eleLoc, i + 1));
}
}
}
return false;
}
```
注:定位重试与定位等待是在一起实现的,功能角度不同。
#### 操作重试
操作重试主要应对操作级别的异常情况,如点击下拉列表未弹出选项值,基础资料未弹出备选数据,当无法定位到下一步待操作元素时重新执行整个操作过程。
```java
public void selectByContent(String arrowEleLoc, String itemLoc) {
for (int i = 0; i < 3; i++) {
try {
SelenideElement element = $x(arrowEleLoc).shouldBe(Condition.exist);
element.click();
sleepBaby(1); //等待下拉列表弹出,为提高稳定性
SelenideElement itemElement = $x(itemLoc).shouldBe(Condition.exist);
itemElement.click();
return;
} catch (Error error) {
if (i < 2) {
log.info(String.format("未找到元素%s,进行第%d次重试...", itemLoc, i + 1));
}
}
}
throw new ElementNotFoundException(String.format("错误:重试2次仍未找到元素%s", itemLoc));
}
```
#### 用例重试
有时测试用例运行失败并不代表产品功能存在缺陷,可能由于执行用例时环境的某些异常情况导致,加入用例重试机制对执行失败的用例连续几次重试,确保都执行失败是真正意义上的产品缺陷,对提高测试准确性和可信度具有较大意义。
##### 重试分析器
TestNg提供**org.testng.IRetryAnalyzer**接口用于指示失败的用例是否需要重试,实现该接口,重写**public boolean retry(ITestResult result)** 接口方法,指定一个计数器控制测试用例重试次数。
```java
package com.luthai.autotest.ui.listener;
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
public class Retry implements IRetryAnalyzer {
private int retryCount = 0;
private int maxRetryCount = 3; // retry a failed test 2 additional times
@Override
public boolean retry(ITestResult result) {
if (retryCount <maxRetryCount) {
retryCount++;
return true;
}
return false;
}
}
```
##### 使用@Test(retryAnalyzer = Retry.class)指定重试分析器
只需在测试用例的测试方法注解@Test中简单指定**retryAnalyzer**属性值即可将重试应用到测试方法上。
```java
@Test(retryAnalyzer = Retry.class)
```

##### 在运行时指定retryAnalyzer
TestNg提供**IAnnotationTransformer**监听器接口,实现此接口并在testng.xml配置文件中将其注册为监听器,即可再测试运行期间动态为每个测试方法添加重试分析器
```java
package com.luthai.autotest.ui.listener;
import org.testng.IAnnotationTransformer;
import org.testng.annotations.ITestAnnotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class RetryListener implements IAnnotationTransformer {
@Override
public void transform(ITestAnnotation annotation, Class testClass,
Constructor