Excel+TestNG+HttpClient+ExtentReport实现接口自动化

@Excel+TestNG+HttpClient+ExtentReport实现接口自动化

这个接口自动化项目基于Java语言,以excel作为主要数据源,httpClient作为发起接口请求的api, TestNG作为测试框架,ExtentReport生成测试报告。

数据源Excel

用excel作为主要数据源,excel中存放接口测试数据的基本信息,比如此条接口测试数据是否要运行,接口的url,接口的请求方式(get/post),接口的请求参数,接口的响应的处理方式
如下图
在这里插入图片描述
之后,利用Apache Poi中的api读取excel中的数据读到内存中的list,将这个list构造成一个泛型是对象的一维数组的list即List<Obejct[]>,将这个list的迭代器iterator作为数据源dataProvider提供给TestNG框架。

 /**
     * 所有api测试用例数据
     */
    protected List<ApiDataBean> dataList = new ArrayList<ApiDataBean>();
    
/**
     * @return void
     * @Author Siyue Chen
     * @Description 将api-data.xls中的接口数据读入内存List<ApiDataBean> dataList
     * @Date 15:52 2023/6/9
     * @Param [excelPath, sheetName]
     **/
    @Parameters({"excelPath", "sheetName"})
    @BeforeTest
    public void readData(@Optional("case/api-data.xls") String excelPath, @Optional("Sheet1") String sheetName) throws DocumentException {
        dataList = readExcelData(ApiDataBean.class, excelPath.split(";"),
                sheetName.split(";"));
    }

    /**
     * 提供TestNG框架中的数据源,一个对象数组的list的迭代器
     * 过滤数据,run标记为Y的执行。
     *
     * @return
     * @throws DocumentException
     */
    @DataProvider(name = "apiDatas")
    public Iterator<Object[]> getApiData(ITestContext context)
            throws DocumentException {
        List<Object[]> dataProvider = new ArrayList<Object[]>();
        for (ApiDataBean data : dataList) {
            if (data.isRun()) {
                dataProvider.add(new Object[]{data});
            }
        }
        return dataProvider.iterator();
    }

    @Test(dataProvider = "apiDatas")
    public void apiTest(ApiDataBean apiDataBean) throws Exception {
        ReportUtil.log("--- test start ---");
        ...
        }

如何处理请求参数中的特殊变量值

请求参数里不一定都是固定不变的参数值,比如"123",会有一些特殊的请求参数,比如B接口的某个请求参数可能是A接口的响应参数,也有可能接口的请求参数是用随机函数生成的字符串,先考虑这些特殊的请求参数。

如何处理关联场景下的请求参数值

B接口的请求参数是A接口的响应参数被称作关联。
这里可以类比Jmeter里的关联场景来处理。

  • 首先是要保存下A接口的响应参数,类似于Jmeter里的json提取器。

在excel里增加一列save,相应的bean里也要有save这一成员属性。
在这里插入图片描述
用正则表达式

([;=]*)=([;]*)

去匹配save字段,等式左边匹配变量名,等式右边匹配jsonPath,程序读取出jsonPath对应的响应值后,以变量名作key,响应参数值作为value存入一个类的全局变量HashMap,可以给这个hashMap起个名字叫公共参数池防止之后和另一个处理函数的HashMap混淆。
这个公共参数池类似于Jmeter的内置参数vars

/**
	 * 公共参数数据池(全局可用)
	 */
	private static Map<String, String> saveDatas = new HashMap<String, String>();


/**过程参数存入公共参数池
	 * 提取json串中的值保存至公共池中
	 * 
	 * @param json
	 *            json响应
	 * @param allSave  也就是excel里的save字段  msg_id=$.jsonPath.xx
	 *            所有将被保存的数据:xx=$.jsonpath.xx;oo=$.jsonpath.oo,将$.jsonpath.
	 *            xx提取出来的值存放至公共池的xx中,将$.jsonpath.oo提取出来的值存放至公共池的oo中
	 */
	protected void saveResult(String json, String allSave) {
		if (null == json || "".equals(json) || null == allSave
				|| "".equals(allSave)) {
			return;
		}
		allSave = getCommonParam(allSave);
		//first_currency=$.result[0].currency;first_name=$.result[0].name
		//将excel里save  msg_id=$.jsonPath.xx;字符串分隔开来
		String[] saves = allSave.split(";");
		String key, value;
		//遍历save  msg_id=$.jsonPath.xx;
		for (String save : saves) {
			//	正则表达式 由0-n个非;=组成的字符串 =  0-n个非;组成的字符串
			//对msg_id=$.jsonPath.xx做正则匹配
			Pattern pattern = Pattern.compile("([^;=]*)=([^;]*)");
			Matcher m = pattern.matcher(save.trim());
			while (m.find()) {
				//将excel save字段里等式左边的key作为key
				//msg_id是匹配到到第一个分组group(1)
				//对左边调用getBuildValue函数可能只起到一个trim的作用
				key = getBuildValue(json, m.group(1));
				//根据excel里save字段的jsonPath提取响应里对应着jsonPath的值作为value
				//$.jsonPath.xx是匹配到到第二个分组group(2)
				value = getBuildValue(json, m.group(2));

				ReportUtil.log(String.format("存储公共参数   %s值为:%s.", key, value));
				saveDatas.put(key, value);
			}
		}
	}
  • 其次是将B接口请求参数中的变量替换为公共参数池中的变量值

B接口请求参数中引用变量的格式参考了Jmeter引用变量的格式。
举个🌰

{
	"userName":"${userName}"
}

程序会在发起请求前处理请求参数,处理请求参数时用正则表达式

\$\{(.*?)\}

去匹配

${paramName}

这种模式的请求参数,匹配到变量名之后,以变量名作key去公共参数池中取值并替换掉${xx}。

/**
	 * 替换符,如果数据中包含“${}”则会被替换成公共参数中存储的数据
	 */
	protected Pattern replaceParamPattern = Pattern.compile("\\$\\{(.*?)\\}");

/**公共参数的取
	 * 取公共参数 并替换参数
	 * 将参数param中的${xxx}替换为公共参数池map中以${xxx}中的xxx作key的value,并返回处理过的参数
	 * @param param
	 * @return
	 */
	protected String getCommonParam(String param) {
		//如果参数为空,返回空字符串
		if (StringUtil.isEmpty(param)) {
			return "";
		}
		//这个正则表达式模式匹配的是${}
		Matcher m = replaceParamPattern.matcher(param);// 取公共参数正则
		while (m.find()) {
			//取匹配到到的所有子字符串中的第一个分组,其实这个正则表达式就只有一个分组,(  ${}  )
			String replaceKey = m.group(1);
			String value;
			// 从公共参数池中获取值,以${xxx}中的xxx作key,取key对应的value
			value = getSaveData(replaceKey);
			// 如果公共参数池中未能找到对应的值,该用例失败。
			Assert.assertNotNull(value,
					String.format("格式化参数失败,公共参数中找不到%s。", replaceKey));
			//将参数中的${xxx}替换为公共参数池中的值
			param = param.replace(m.group(), value);
		}
		return param;
	}

如何处理函数生成的请求参数值

有一些请求参数值,需要用函数生成,比如如果需要随机生成用户名,或者对密码进行MD5加密的话。
函数生成的请求参数格式参考了Jmeter的函数格式,__funName(arg1,arg2,…)

  • 首先要在程序里定义好函数
    函数接口,两个抽象方法,分别是获取函数名和函数的具体执行方法excute()方法
package com.sen.api.functions;

/*
函数分成两个部分,第一个部分是函数的名字,第二个部分是函数的具体执行
 */
public interface Function {
	/*
	函数的具体执行
	args是函数的参数
	 */
	String execute(String[] args);
/*
函数的名字
 */
	String getReferenceKey();
}

所有的函数都去实现这个函数接口
举个🌰,随机函数

package com.sen.api.functions;

import com.sen.api.utils.RandomUtil;

/*
定义一个随机函数,__random()
 */
public class RandomFunction implements Function {

	/**
	 * @Author Siyue Chen
	 * @Description 如果__random()无参数,默认生成一个长度为6 [a-zA-Z0-9]{6}的字符串;__random(arg1)arg1指定随机字符串长度;
	 * __random(arg1,arg2)第二个参数指定了随机字符串是否只由数字组成
	 * @Date 15:03 2023/6/6
	 * @Param
	 * @return
	 **/
	@Override
	public String execute(String[] args) {
		int len = args.length;
		int length = 6;// 默认为6
		boolean flag = false;// 默认为false
		//如果有参数
		if (len > 0) {// 第一个参数字符串长度
			length = Integer.valueOf(args[0]);
		}
		//如果有2个及以上参数
		if (len > 1) {// 第二个参数是否纯字符串
			flag = Boolean.valueOf(args[1]);
		}
		return RandomUtil.getRandom(length, flag);
	}

	@Override
	public String getReferenceKey() {
		return "random";
	}

}

MD5加密函数

package com.sen.api.functions;

import java.io.File;
import java.io.FileInputStream;
import java.net.URL;

import org.apache.commons.codec.digest.DigestUtils;

public class Md5Function implements Function{

	/*
	定义一个名为__md5(文件路径)的函数,获取文件的md5码
	 */
	@Override
	public String execute(String[] args) {
		try {
			String filePath = args[0];
			if (filePath.startsWith("http")) {
				return DigestUtils.md5Hex(new URL(filePath).openStream());
			} else {
				return DigestUtils.md5Hex(new FileInputStream(new File(
						filePath)));
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public String getReferenceKey() {
		// TODO Auto-generated method stub
		return "md5";
	}

}

  • 定义好函数之后,规定在excel里用__funName(arg1,arg2,…)这种特殊格式的字符串去标识某个请求参数值是用函数生成的,程序就要去识别这种特殊格式的字符串,也就是用正则表达式去匹配到函数名和函数的参数

/**
	 * 截取自定义方法正则表达式:__xxx(param1,param2,...)
	 */
	protected Pattern funPattern = Pattern
			.compile("__(\\w*?)\\((([\\w\\\\\\/:\\.\\$]*,?)*)\\)");
/**
	 * 组件预参数(处理__fucn()以及${xxxx})
	 * 
	 * @param apiDataBean
	 * @return
	 */
	protected String buildParam(String param) {
		// 处理${}
		param = getCommonParam(param);
		//这个funPattern是指__fun(xxx,...)的正则匹配模式
		Matcher m = funPattern.matcher(param);
		while (m.find()) {
			//匹配到的子字符串的第一个分组是(\w*?),也就是函数名
			String funcName = m.group(1);
			//匹配到的子字符串的第二个分组是(([\w\\\/:\$\.]*,?)*),也就是param1,param2...
			String args = m.group(2);
			String value;
			// bodyfile属于特殊情况,不进行匹配,在post请求的时候进行处理
			if (FunctionUtil.isFunction(funcName)
					&& !funcName.equals("bodyfile")) {
				// 属于函数助手,调用那个函数助手获取。
				value = FunctionUtil.getValue(funcName, args.split(","));
				// 解析对应的函数失败
				Assert.assertNotNull(value,
						String.format("解析函数失败:%s。", funcName));
				param = StringUtil.replaceFirst(param, m.group(), value);
			}
		}
		return param;
	}

上面有一个if判断,根据函数名判断是否是程序里定义的函数以及 直接根据函数名和参数值获取到函数执行结果的,要想实现根据函数名和参数值获取到函数执行结果,需要借助反射机制

大致思路是:将函数名作为key,函数接口实现类的反射对象作为value存入到一个类全局的静态的hashMap里,这个哈希map我们可以叫它函数池,以将它与公共参数池区分开来,之后便可以直接根据函数名作为key,取到函数接口实现类的反射对象,再用newInstance()方法new一个函数接口实现类的对象实例,再用对象实例去调用函数接口实现类的执行方法excute();
如下:

//key为函数名,value为com.sen.api.functions路径下所有继承/实现/等于Function.class的类的反射对象
	private static final Map<String, Class<? extends Function>> functionsMap = new HashMap<String, Class<? extends Function>>();

 /**
	  * @Author Siyue Chen
	  * @Description 执行这个函数,返回函数执行结果
	  * @Date 09:50 2023/6/9
	  * @Param [functionName, 函数名
	  * args]函数的参数
	  * @return java.lang.String
	  **/
	public static String getValue(String functionName,String[] args){
		try {
			//通过函数名取到这个函数类的反射对象,构造一个新的实例对象,并且调用实例的excute方法
			return functionsMap.get(functionName).newInstance().execute(args);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return "";
		}
	}

这里还有一个很关键的地方,就是functionsMap是如何形成的?
大致思路是:

首先获取到函数接口所在包的全限定名cls.getPackage().getName(),之后调用应用程序类加载的getResource(包的全限定名).getPath()的方法获取到函数接口所在包的绝对路径,之后去遍历绝对路径下的所有文件,如果有文件是以.class后缀结尾的,那么就用Class.forName(“com.xx”)去生成类的反射对象,之后再去判断所有类的反射对象是否实现了函数接口cls.isAssignableFrom©,如果实现类函数接口,那么这个类一定是函数接口实现类,到此已经获得了所有的函数接口实现类的反射对象;
之后写一个静态代码块初始化一下函数池就好,用函数接口实现类的反射对象newInstance()一个函数接口实现类的对象实例,用对象实例去调用获取函数名的方法,将函数名作为key,函数接口实现类的反射对象作为value存入函数池,函数池到此初始化完成。✌️


static {
		//bodyfile 特殊处理
		functionsMap.put("bodyfile", null);
		//获取Function.class所在包com.sen.api.functions路径下所有继承/实现/等于Function.class的类的反射对象
		List<Class<?>> clazzes = ClassFinder.getAllAssignedClass(Function.class);
		clazzes.forEach((clazz) -> {
			try {
				//通过构造clazz对象的实例去调用getRefrenceKey方法
				//tips:在反射中,一定要构造一个实例才能调用类中的方法,即便是method.invoke(object)也是如此
				// function
				Function tempFunc = (Function) clazz.newInstance();
				String referenceKey = tempFunc.getReferenceKey();
				if (referenceKey.length() > 0) { // ignore self
					functionsMap.put(referenceKey, tempFunc.getClass());
				}
			} catch (Exception ex) {
				ex.printStackTrace();
				//TODO 
			}
		});
	}
/** 
     * 获取同一路径下所有子类或接口实现类 
     *  获取到cls所在包路径下所有继承/实现/等于cls的类的反射对象
     * @param intf 
     * @return 
     * @throws IOException 
     * @throws ClassNotFoundException 
     */  
    public static List<Class<?>> getAllAssignedClass(Class<?> cls) {
        /*A.isAssignableFrom(B)
        A和B均为Class对象,判断B是否等于/继承/实现A,是返回true,否返回false*/
        List<Class<?>> classes = new ArrayList<Class<?>>();
//        获取到当前类所在包下所有类到反射对象
        for (Class<?> c : getClasses(cls)) {
            //如果有类=/继承/实现当前类cls,那么将这个类加入到返回列表中
            if (cls.isAssignableFrom(c) && !cls.equals(c)) {  
                classes.add(c);  
            }  
        }  
        return classes;  
    }  
/** 
     * 取得当前类所在路径下的所有类 的反射对象
     * 获取到当前类所在包路径下的所有类的反射对象包括自身
     * @param cls 
     * @return 
     * @throws IOException 
     * @throws ClassNotFoundException 
     */  
    public static List<Class<?>> getClasses(Class<?> cls) {
        //首先获取类的包路径
        String pk = cls.getPackage().getName();  
        String path = pk.replace('.', '/');  
//      URL url = classloader.getResource(path);  
//      return getClasses(new File(url.getFile()), pk);  
      try {
          //接着通过包路径获取到包所在的绝对路径
			String dirPath = URLDecoder.decode(classloader.getResource(path).getPath(),"utf-8");
			return getClasses(new File(dirPath), pk);
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
      return new ArrayList<Class<?>>();
    }  

 /** 
     * 迭代查找类 
     *  获取到pk包路径(也就是绝对路径dir)下的所有类的反射对象
     * @param dir 一个用包的绝对路径作为参数构造的目录
     * @param pk 包的路径 com.xx
     * @return 
     * @throws ClassNotFoundException 
     */  
    private static List<Class<?>> getClasses(File dir, String pk) {  
        List<Class<?>> classes = new ArrayList<Class<?>>();
        //如果包的不存在,那么返回空列表
        if (!dir.exists()) {  
            return classes;  
        }
        //开始遍历包下面每一个文件、目录
        for (File f : dir.listFiles()) {
            //如果是目录的话递归调用当前方法
            if (f.isDirectory()) {  
                classes.addAll(getClasses(f, pk + "." + f.getName()));  
            }
            //如果是文件,获取文件名
            String name = f.getName();
            //如果是类的二进制文件,那么通过反射方法Class.forName获取到类的反射对象,并且将其加入到返回列表中
            if (name.endsWith(".class")) {
            	try{
                classes.add(Class.forName(pk + "." + name.substring(0, name.length() - 6)));
                }catch(Exception ex){
                	//TODO console warn
                }
            }  
        }  
        return classes;  
    }  

发起请求

在处理好请求参数之后,就开始调用HttpClient发起http请求了

@Test(dataProvider = "apiDatas")
    public void apiTest(ApiDataBean apiDataBean) throws Exception {
        ReportUtil.log("--- test start ---");
//处理请求参数
        String apiParam = buildRequestParam(apiDataBean);
        // 封装请求方法
        HttpUriRequest method = parseHttpRequest(apiDataBean.getUrl(),
                apiDataBean.getMethod(), apiParam);
        String responseData;
        try {
            // 执行
            HttpResponse response = client.execute(method);
            int responseStatus = response.getStatusLine().getStatusCode();
            ReportUtil.log("返回状态码:" + responseStatus);
            if (apiDataBean.getStatus() != 0) {
                Assert.assertEquals(responseStatus, apiDataBean.getStatus(),
                        "返回状态码与预期不符合!");
            }
            HttpEntity respEntity = response.getEntity();
            responseData = EntityUtils.toString(respEntity, "UTF-8");
             // 输出返回数据log
        ReportUtil.log("resp:" + responseData);
        // 验证预期信息
        verifyResult(responseData, apiDataBean.getVerify(),
                apiDataBean.isContains());

        // 对返回结果进行提取保存。
        saveResult(responseData, apiDataBean.getSave());
    }

发起http请求得到响应之后,就需要判断响应是否符合预期了

如何进行响应断言

主要写了两种判断响应报文是否符合预期的方式

  • 第一种,包含模式,类似jmeter的响应断言里的包括模式;
    判断响应报文是否包含某个字符串
    这种响应验证方式在excel里是这样标识的。
    在这里插入图片描述
// 验证预期信息
        verifyResult(responseData, apiDataBean.getVerify(),
                apiDataBean.isContains());

public class AssertUtil {

	public static void contains(String source, String search) {
		Assert.assertTrue(source.contains(search),
				String.format("期待'%s'包含'%s',实际为不包含.", source, search));
	}
}

/**
	 * @Author Siyue Chen
	 * @Description 验证接口响应结果
	 * @Date 14:14 2023/6/9
	 * @Param [sourchData,
	 * verifyStr, $.jsonPath.xx="abc";$.jsonPath.yy=${mm};
	 * contains]
	 * @return void
	 **/
	protected void verifyResult(String sourchData, String verifyStr,
			boolean contains) {
		if (StringUtil.isEmpty(verifyStr)) {
			return;
		}
		//将需要判定的内容中的${}替换成公共参数池中的值
		String allVerify = getCommonParam(verifyStr);
		ReportUtil.log("验证数据:" + allVerify);
		//如果是以验证响应中包含verify字段内容的话
		if (contains) {
			// 验证结果包含
			AssertUtil.contains(sourchData, allVerify);
		} 
		//else里是第二种验证反方式了
		else {...}
	}

  • 第二种验证方式:类似于jmeter的json断言,判断某个jsonPath下的参数值是否等于预期值
//书接上文
else {
			//如果是以$.jsonPath.xx="abc"这种方式验证响应结果,而不是验证响应结果中包含verify的话
			Pattern pattern = Pattern.compile("([^;]*)=([^;]*)");
			Matcher m = pattern.matcher(allVerify.trim());
			while (m.find()) {
				//group(1)是等式左边的,需要去真实响应中获取jsonPath对应的真实值
				String actualValue = getBuildValue(sourchData, m.group(1));
				//group(2)是等式右边的
				String exceptValue = getBuildValue(sourchData, m.group(2));
				ReportUtil.log(String.format("验证转换后的值%s=%s", actualValue,
						exceptValue));
				Assert.assertEquals(actualValue, exceptValue, "验证预期结果失败。");
			}
		}
		

Extent Report生成测试报告

引入依赖

		<dependency>
			<groupId>com.aventstack</groupId>
			<artifactId>extentreports</artifactId>
			<version>3.0.3</version>
		</dependency>

监听器

package com.sen.api.listeners;

import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.ResourceCDN;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.model.Log;
import com.aventstack.extentreports.model.TestAttribute;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.aventstack.extentreports.reporter.configuration.ChartLocation;
import com.aventstack.extentreports.reporter.configuration.Theme;
import com.sen.api.utils.ReportUtil;
import org.testng.*;
import org.testng.xml.XmlSuite;

import java.io.File;
import java.util.*;

//测试报告监听器
public class ExtentTestNGIReporterListener implements IReporter {
    //生成的路径以及文件名
    private static final String OUTPUT_FOLDER = "test-output/";
    private static final String FILE_NAME = "index.html";

    private ExtentReports extent;

    @Override
    public void generateReport(List<XmlSuite>  xmlSuites, List<ISuite> suites, String outputDirectory) {
        init();
        boolean createSuiteNode = false;
        if(suites.size()>1){
            createSuiteNode=true;
        }
        for (ISuite suite : suites) {
            Map<String, ISuiteResult>  result = suite.getResults();
            //如果suite里面没有任何用例,直接跳过,不在报告里生成
            if(result.size()==0){
                continue;
            }
            //统计suite下的成功、失败、跳过的总用例数
            int suiteFailSize=0;
            int suitePassSize=0;
            int suiteSkipSize=0;
            ExtentTest suiteTest=null;
            //存在多个suite的情况下,在报告中将同一个一个suite的测试结果归为一类,创建一级节点。
            if(createSuiteNode){
                suiteTest = extent.createTest(suite.getName()).assignCategory(suite.getName());
            }
            boolean createSuiteResultNode = false;
            if(result.size()>1){
                createSuiteResultNode=true;
            }
            Date suiteStartTime = null,suiteEndTime=new Date();
            for (ISuiteResult r : result.values()) {
                ExtentTest resultNode;
                ITestContext context = r.getTestContext();
                if(createSuiteResultNode){
                    //没有创建suite的情况下,将在SuiteResult的创建为一级节点,否则创建为suite的一个子节点。
                    if( null == suiteTest){
                        resultNode = extent.createTest(context.getName());
                    }else{
                        resultNode = suiteTest.createNode(context.getName());
                    }
                }else{
                    resultNode = suiteTest;
                }
                if(resultNode != null){
                    resultNode.assignCategory(suite.getName(),context.getName());
                    if(suiteStartTime == null){
                        suiteStartTime = context.getStartDate();
                    }
                    suiteEndTime = context.getEndDate();
                    resultNode.getModel().setStartTime(context.getStartDate());
                    resultNode.getModel().setEndTime(context.getEndDate());
                    //统计SuiteResult下的数据
                    int passSize = context.getPassedTests().size();
                    int failSize = context.getFailedTests().size();
                    int skipSize = context.getSkippedTests().size();
                    suitePassSize += passSize;
                    suiteFailSize += failSize;
                    suiteSkipSize += skipSize;
                    if(failSize>0){
                        resultNode.getModel().setStatus(Status.FAIL);
                    }
                    resultNode.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",passSize,failSize,skipSize));
                }
                buildTestNodes(resultNode,context.getFailedTests(), Status.FAIL);
                buildTestNodes(resultNode,context.getSkippedTests(), Status.SKIP);
                buildTestNodes(resultNode,context.getPassedTests(), Status.PASS);
            }
            if(suiteTest!= null){
                suiteTest.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",suitePassSize,suiteFailSize,suiteSkipSize));
                suiteTest.getModel().setStartTime(suiteStartTime==null?new Date():suiteStartTime);
                suiteTest.getModel().setEndTime(suiteEndTime);
                if(suiteFailSize>0){
                    suiteTest.getModel().setStatus(Status.FAIL);
                }
            }

        }
        extent.flush();
    }

    private void init() {
        //文件夹不存在的话进行创建
        File reportDir= new File(OUTPUT_FOLDER);
        if(!reportDir.exists()&& !reportDir .isDirectory()){
            reportDir.mkdir();
        }
        ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
        htmlReporter.config().setDocumentTitle(ReportUtil.getReportName());
        htmlReporter.config().setReportName(ReportUtil.getReportName());
        htmlReporter.config().setChartVisibilityOnOpen(true);
        htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
        htmlReporter.config().setTheme(Theme.STANDARD);
        //设置点击效果:.node.level-1  ul{ display:none;} .node.level-1.active ul{display:block;}
        //设置系统信息样式:.card-panel.environment  th:first-child{ width:30%;}
        htmlReporter.config().setCSS(".node.level-1  ul{ display:none;} .node.level-1.active ul{display:block;}  .card-panel.environment  th:first-child{ width:30%;}");
        // 移除按键监听事件
        htmlReporter.config().setJS("$(window).off(\"keydown\");");
        //设置静态文件的DNS 如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS 或者ResourceCDN.GITHUB
        htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
        extent = new ExtentReports();
        extent.attachReporter(htmlReporter);
        extent.setReportUsesManualConfiguration(true);
        // 设置系统信息
        Properties properties = System.getProperties();
        extent.setSystemInfo("os.name",properties.getProperty("os.name","未知"));
        extent.setSystemInfo("os.arch",properties.getProperty("os.arch","未知"));
        extent.setSystemInfo("os.version",properties.getProperty("os.version","未知"));
        extent.setSystemInfo("java.version",properties.getProperty("java.version","未知"));
        extent.setSystemInfo("java.home",properties.getProperty("java.home","未知"));
        extent.setSystemInfo("user.name",properties.getProperty("user.name","未知"));
        extent.setSystemInfo("user.dir",properties.getProperty("user.dir","未知"));
    }

    private void buildTestNodes(ExtentTest extenttest,IResultMap tests, Status status) {
        //存在父节点时,获取父节点的标签
        String[] categories=new String[0];
        if(extenttest != null ){
            List<TestAttribute> categoryList = extenttest.getModel().getCategoryContext().getAll();
            categories = new String[categoryList.size()];
            for(int index=0;index<categoryList.size();index++){
                categories[index] = categoryList.get(index).getName();
            }
        }

        ExtentTest test;

        if (tests.size() > 0) {
            //调整用例排序,按时间排序
            Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() {
                @Override
                public int compare(ITestResult o1, ITestResult o2) {
                    return o1.getStartMillis()<o2.getStartMillis()?-1:1;
                }
            });
            treeSet.addAll(tests.getAllResults());
            for (ITestResult result : treeSet) {
                Object[] parameters = result.getParameters();
                String name="";
                //如果有参数,则使用参数的toString组合代替报告中的name
                for(Object param:parameters){
                    name+=param.toString();
                }
                if(name.length()>0){
                    if(name.length()>50){
                        name= name.substring(0,49)+"...";
                    }
                }else{
                    name = result.getMethod().getMethodName();
                }
                if(extenttest==null){
                    test = extent.createTest(name);
                }else{
                    //作为子节点进行创建时,设置同父节点的标签一致,便于报告检索。
                    test = extenttest.createNode(name).assignCategory(categories);
                }
                //test.getModel().setDescription(description.toString());
                //test = extent.createTest(result.getMethod().getMethodName());
                for (String group : result.getMethod().getGroups())
                    test.assignCategory(group);

                List<String> outputList = Reporter.getOutput(result);
                for(String output:outputList){
                    //将用例的log输出报告中
                    test.debug(output.replaceAll("<","&lt;").replaceAll(">","&gt;"));
                }
                if (result.getThrowable() != null) {
                    test.log(status, result.getThrowable());
                }
                else {
                    test.log(status, "Test " + status.toString().toLowerCase() + "ed");
                }
                //设置log的时间,根据ReportUtil.log()的特定格式进行处理获取数据log的时间
                Iterator logIterator = test.getModel().getLogContext().getIterator();
                while (logIterator.hasNext()){
                    Log log = (Log) logIterator.next();
                    String details = log.getDetails();
                    if(details.contains(ReportUtil.getSpiltTimeAndMsg())){
                        String time = details.split(ReportUtil.getSpiltTimeAndMsg())[0];
                        log.setTimestamp(getTime(Long.valueOf(time)));
                        log.setDetails(details.substring(time.length()+ReportUtil.getSpiltTimeAndMsg().length()));
                    }else{
                        log.setTimestamp(getTime(result.getEndMillis()));
                    }
                }
                test.getModel().setStartTime(getTime(result.getStartMillis()));
                test.getModel().setEndTime(getTime(result.getEndMillis()));
            }
        }
    }

    private Date getTime(long millis) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);
        return calendar.getTime();
    }
}

最后xml文件里引入一下监听器

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="接口自动化测试" verbose="1" preserve-order="true" parallel="false">
	<test name="自动化测试用例">
		<parameter name="excelPath" value="case/api-data.xls"></parameter>
		<parameter name="sheetName" value="Sheet1"></parameter>
		<classes>
			<class name="test.com.sen.api.ApiTest">
				<methods>							
					<include name="apiTest"></include>
				</methods>
			</class>	
		</classes>
	</test>
	<listeners>	
		<listener class-name="com.sen.api.listeners.AutoTestListener"></listener>
		<listener class-name="com.sen.api.listeners.RetryListener"></listener>
		<!-- ExtentReport 报告  -->
		<listener class-name="com.sen.api.listeners.ExtentTestNGIReporterListener"></listener>
	</listeners>
</suite> 

如何实现测试用例失败自动重跑机制

TestNG如何处理失败的测试结果
如果测试结果被断言为失败,TestNG会调用IRetryAnalyzer的retry()方法,如果retry方法返回true,那么就会重新执行这个测试用例,如果retry方法为false,那么就不会再执行这个失败的测试用例;
如下所示

IRetryAnalyzer retryAnalyzer = testMethod.getRetryAnalyzer();
boolean willRetry = retryAnalyzer != null && status == ITestResult.FAILURE && failure.instances != null && retryAnalyzer.retry(testResult);
if (willRetry) {
  resultsToRetry.add(testResult);
  failure.count++;
  failure.instances.add(testResult.getInstance());
  testResult.setStatus(ITestResult.SKIP);
} else {
  testResult.setStatus(status);
  if (status == ITestResult.FAILURE && !handled) {
    handleException(ite, testMethod, testResult, failure.count++);
  }

所以如果我们想要让失败的测试用例自动重新运行,需要重写IRetryAnalyzer的retry方法,自己定义返回true/false的规则

package com.sen.api.listeners;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
import org.testng.Reporter;

import com.sen.api.excepions.ErrorRespStatusException;

//失败用例重新执行
public class TestngRetry implements IRetryAnalyzer {

    private int count = 1;
    private int max_count = 3;

    @Override
    public boolean retry(ITestResult result) {
        System.out.println("Test case :" + result.getName() + ",retry time: " + count + "");
        if (count < max_count) {
            count++;
            return true;
        }else{
            resetRetryCount();
        }
        return false;
    }

	public  void resetRetryCount() {
		count = 1;
	}
}


之后还需要将这个重写过的IRetryAnalyzer接口实现类放到@Test属性中

@Test(retryAnalyzer=RetryImpl.class)

但这也造成了一个问题,就是如果有多个测试方法,难道每个@Test注解里都要写上这个属性吗?
有一个接口IAnnotationTransformer,接口有一个抽象方法transform可以修改@Test属性。

package com.sen.api.listeners;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.annotations.ITestAnnotation;

public class RetryListener implements IAnnotationTransformer {
	@SuppressWarnings("rawtypes")
	public void transform(ITestAnnotation annotation, Class testClass,
			Constructor testConstructor, Method testMethod) {
		IRetryAnalyzer retry = annotation.getRetryAnalyzer();
		if (retry == null) {
			annotation.setRetryAnalyzer(TestngRetry.class);
		}
	}
}

之后只需要在xml文件里加上这个监听器,或者在测试类中通过@Listeners({RetryListener.class})既可

@Listeners({RetryListener.class})
public class ApiTest extends TestBase 
<listeners>	
		<listener class-name="com.sen.api.listeners.RetryListener"></listener>
</listeners>

测试报告最后如下
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值