目录
背景:
场景一:
场景二:
一、单独case打标签,这个case失败就报警
二、给运行入口打报警标签,批量进行报警
1、批量case运行入口
2、DingTalkAlarm 注解
3、CaseSelectorExtension 筛选、运行case的扩展类
4、FailureListener 报警的Listener
5、AlarmCallBack 报警接口
6、DefaultAlarmCallBack 默认报警方式
三、具体实现报警
1、AlarmFacade 报警模式
2、具体钉钉的报警方法AlarmService
背景:
实现功能,case执行失败后,触发钉钉报警。
场景一:
每条case打一个报警的标签,只要这条case执行失败,触发报警。
缺点:
如果每条case都有报警的标签,不同的场景,同一条case执行报警的策略也不同,如A场景场景,case1需要报警,在b场景,case1不需要报警
场景二:
不在每条case上打报警标签,而是在批量筛选case的入口,打报警标签。这一批失败的case,触发报警。
这样就可以根据执行的业务不同,来触发报警。
---------------------------------------------------------------------------
一、单独case打标签,这个case失败就报警
我们先完成场景一的功能。
1、给具体case打报警注解@DingTalkAlarm
@AutoTest // 就不需要使用框架自带注解@Test,使用我们自定义注解
@CaseDesc(desc="1111",owner = "111") // 描述和管理人
@CaseTitle("xx") // case的标题
@CheckPoint("aaa") // 检查点,可以是多个
@CheckPoint("bbb")
@CaseTag(key = "level",val = "redLine") // 用于扩展注解,自己想放啥放啥
@CaseTag(key = "level",val = "P1")
@CaseGroup(team="dev",group="normal")
@DingTalkAlarm
public void test2(){
System.out.println("TestLogin.test2");
Assertions.assertEquals(1,2);
}
2、写注解
DingTalkAlarm
package com.example.autoapi.annotation;
import com.example.autoapi.extension.AlarmExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD}) // Target表示将来这个注解应用在哪些方面。注解可以应用在类上、方法上(此处只应用在方法上)
@Retention(RetentionPolicy.RUNTIME) //运行时,使用这个这个注解
@ExtendWith(AlarmExtension.class) // 与junit结合,由junit提供。自定义一个扩展类,然后放到这里。走我们的自定义类
public @interface DingTalkAlarm {
}
3、注解DingTalkAlarm的扩展类AlarmExtension
AlarmExtension
package com.example.autoapi.extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
//TestExecutionExceptionHandler 框架提供的能力,测试执行失败,触发的回调
public class AlarmExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable {
System.out.println("AlarmExtension.handleTestExecutionException");
}
}
注意:
我们的扩展类,一定要实现TestExecutionExceptionHandler接口。
TestExecutionExceptionHandler,框架提供的能力,测试执行失败,触发的回调。
继承这个类后,当打标签的case执行失败后,就会触发执行这个扩展类。
(这里先简单打印信息,不做具体的报警处理)
整体流程:
------------------------------------------------------------------------------------------------------------------------
二、给运行入口打报警标签,批量进行报警
这是大概的整个流程
1、批量case运行入口
@CaseSelector(scanPackage = "com.example.autoapi.cases.login",key="level",val="redLine")
@DingTalkAlarm(alarmCallBack = DefaultAlarmCallBack.class)
public void redLine(){
}
@CaseSelector 筛选,并运行case
@DingTalkAlarm(alarmCallBack = DefaultAlarmCallBack.class) 报警注解,
a、表示这批case失败后,要触发报警
b、alarmCallBack = DefaultAlarmCallBack.class 。具体的报警方式执行DefaultAlarmCallBack.class。
2、DingTalkAlarm 注解
package com.example.autoapi.annotation;
import com.example.autoapi.alarm.callback.AlarmCallBack;
import com.example.autoapi.extension.AlarmExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD}) // Target表示将来这个注解应用在哪些方面。注解可以应用在类上、方法上(此处只应用在方法上)
@Retention(RetentionPolicy.RUNTIME) //运行时,使用这个这个注解
@ExtendWith(AlarmExtension.class) // 与junit结合,由junit提供。自定义一个扩展类,然后放到这里。走我们的自定义类
public @interface DingTalkAlarm {
Class<? extends AlarmCallBack> alarmCallBack();
}
a、@ExtendWith(AlarmExtension.class)
注解DingTalkAlarm的扩展类。只适应于具体某个case。在某个具体case上打报警注解,然后对应case执行的是这个报警扩展类。
批量执行case,报警,每条case是没有写报警注解的,报警注解写在了运行入口。
b、所以批量运行case,报警的逻辑在CaseSelector 筛选case的扩展类里执行的。
这里注解里有一个属性。
Class<? extends AlarmCallBack> alarmCallBack();
这里需要传一个class,这个class必须继承于AlarmCallBack。
AlarmCallBack 是一个接口,所有报警回调的接口。我们自己设计的,场景就是,我们有多种报警方式,比如邮件报警、企业微信报警、钉钉报警。 我定义一个报警的接口,然后具体的这几个报警都要实现我的这个接口。这样便于统一管理调用各种报警。
这里传的class,就是具体的报警。比如,我们向要钉钉报警,那么这里就穿具体钉钉报警的class
3、CaseSelectorExtension 筛选、运行case的扩展类
package com.example.autoapi.extension;
import com.example.autoapi.alarm.FailureListener;
import com.example.autoapi.alarm.callback.AlarmCallBack;
import com.example.autoapi.annotation.CaseSelector;
import com.example.autoapi.annotation.DingTalkAlarm;
import com.example.autoapi.extension.filter.CaseGroupFilter;
import com.example.autoapi.extension.filter.CaseTagFilter;
import com.example.autoapi.util.RequireUtil;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import java.lang.reflect.Method;
public class CaseSelectorExtension implements BeforeTestExecutionCallback{
@Override
public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
// 获取执行入口的方法
Method method = extensionContext.getRequiredTestMethod();
// 获取注解CaseSelector信息
CaseSelector caseSelector = method.getAnnotation(CaseSelector.class);
// 验证/校验 注解信息
verify(caseSelector);
//----开始进行是筛选----
// 先筛选包
// 再按照@CaseTag key value筛选
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
// 根据包筛选case
.selectors(DiscoverySelectors.selectPackage(caseSelector.scanPackage()))
// 根据注解CaseTag筛选case
.filters(new CaseTagFilter(caseSelector))
.filters(new CaseGroupFilter(caseSelector))
.build();
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener summaryGeneratingListener = new SummaryGeneratingListener();
// 判断执行入口,是否有报警注解
boolean dingTalkSet = method.isAnnotationPresent(DingTalkAlarm.class);
if(dingTalkSet){
// 获取报警注解的属性
// 我们把具体的报警方式,写在了这个注解的属性里
DingTalkAlarm dingTalkAlarm = method.getAnnotation(DingTalkAlarm.class);
Class<? extends AlarmCallBack> alarmCallBack = dingTalkAlarm.alarmCallBack();
FailureListener failureListener = new FailureListener(alarmCallBack);
launcher.execute(request,summaryGeneratingListener,failureListener );
} else {
launcher.execute(request,summaryGeneratingListener );
}
// ----以上固定结构,框架提供能力
// 根据CaseTag 筛选,这里是需要自己写的CaseTagFilter
TestExecutionSummary summary = summaryGeneratingListener.getSummary();
System.out.println("summary.getTestsFoundCount() = " + summary.getTestsFoundCount());
}
private void verify(CaseSelector caseSelector){
RequireUtil.requireNotNullOrEmpty(caseSelector.scanPackage(),"scanPackage is must");
}
}
1、拿到运行入口具体运行的哪个方法
Method method = extensionContext.getRequiredTestMethod();
2、拿到这个方法,就可以拿到这个方法上的批量筛选case的注解与报警注解的信息
3、执行批量筛选case-》再判断这批case是否执行报警。
如果执行报警,则
// 获取报警注解的属性
// 我们把具体的报警方式,写在了这个注解的属性里
DingTalkAlarm dingTalkAlarm = method.getAnnotation(DingTalkAlarm.class);
Class<? extends AlarmCallBack> alarmCallBack = dingTalkAlarm.alarmCallBack();
FailureListener failureListener = new FailureListener(alarmCallBack);
launcher.execute(request,summaryGeneratingListener,failureListener );
执行具体的case运行
launcher.execute(request,summaryGeneratingListener,failureListener );
这里failureListener是我们自定义的报警的linstener。把具体的报警方式传如failureListener中。
4、FailureListener 报警的Listener
package com.example.autoapi.alarm;
import com.example.autoapi.alarm.callback.AlarmCallBack;
import com.example.autoapi.model.FailureResult;
import com.example.autoapi.util.ReflectUtils;
import lombok.AllArgsConstructor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import java.util.Optional;
// case运行完之后的回调 TestExecutionListener
@AllArgsConstructor
public class FailureListener implements TestExecutionListener {
private Class<? extends AlarmCallBack> alarmCallBack;
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
TestExecutionResult.Status status = testExecutionResult.getStatus();
// 判断,若case 没有运行失败,则过滤掉。我们只做 失败case的报警
if(status != TestExecutionResult.Status.FAILED){
return;
}
// 我们从TestIdentifier testIdentifier 中获取具体需要报警的信息,比如case标题等等
//Optional 优雅的处理none
// 如果没有直接return
Optional<TestSource> source = testIdentifier.getSource();
if(!source.isPresent()){
return;
}
TestSource testSource = source.get();
// testSource instanceof MethodSource
// 框架,不仅仅提供了具体case/方法的methodsource,还提供了这个case所属类的classsource。
// 为啥要做这个判断,因为有的testSource是MethodSource,有的是ClassSource
// 此处,我们只需要收集 具体case/方法的信息就可以了
if(!(testSource instanceof MethodSource)){
return;
}
// 强转成methodSource
MethodSource methodSource = (MethodSource)testSource;
Throwable throwable = testExecutionResult.getThrowable().get();
// 将具体的一条失败case相关信息封装在FailureResult中
FailureResult failureResult = FailureResult.builder()
.className(methodSource.getClassName()) //报错的 类名
.methodName(methodSource.getMethodName()) //报错的方法名/case名
.parameterTypes(methodSource.getMethodParameterTypes()) // 传参类型
.throwable(throwable) // 报错原因
.build();
ReflectUtils.newInstance(alarmCallBack).postAlarm(failureResult);
/*
ReflectUtils.newInstance(alarmCallBack).postAlarm(failureResult);
与下面一致
只是,自己写了一个工具而已,反射,通过class拿到实例。需要处理异常
try {
AlarmCallBack alarmCallBack = this.alarmCallBack.newInstance();
alarmCallBack.postAlarm(failureResult);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
*/
}
}
1、FailureListener 要实现 TestExecutionListener这个接口。框架提供的能力
实现executionFinished 这个方法。具体逻辑就是当我们批量执行case时,每条case执行完,都会执行一次 TestExecutionListener 回调中的executionFinished 。
2、每条case执行完,都会执行一次executionFinished方法,则我们需要判断当前这条case是否执行失败
3、判断是否有报错信息,我们报警需要的信息,比如case的名字、id、检查点等等之类的
4、将报警信息封装在类中
5、然后触发报警
5、AlarmCallBack 报警接口
将来,所有的具体报警方式都要实现AlarmCallBack这个接口,为啥中间要写这么一个接口,而不是直接写报警方式。
比如:有钉钉报警、企业微信报警,及其他各种报警方式,我们将所有的具体报警方式都要实现中这个接口的报警类,的报警方法,是为了便于管理各种报警方式
package com.example.autoapi.alarm.callback;
import com.example.autoapi.model.FailureResult;
// 定义回调的接口
// 错误信息
public interface AlarmCallBack {
void postAlarm(FailureResult failureResult);
}
6、DefaultAlarmCallBack 默认报警方式
package com.example.autoapi.alarm.callback;
import com.example.autoapi.alarm.AlarmFacade;
import com.example.autoapi.model.FailureResult;
// 错误信息回调
// 默认的实现类
public class DefaultAlarmCallBack implements AlarmCallBack{
@Override
public void postAlarm(FailureResult failureResult) {
AlarmFacade.doAlarm(failureResult);
}
}
三、具体实现报警
1、AlarmFacade 报警模式
AlarmFacade ,里面是静态的报警方法,可以直接调用报警。
我们将各种具体的报警方式都写在这里。如钉钉报警、微信报警、邮件报警等。
package com.example.autoapi.alarm;
import com.example.autoapi.alarm.service.AlarmService;
import com.example.autoapi.model.FailureResult;
// 这里是用来做报警扩展的
// 如支持钉钉报警、企业微信报警等
// 本次只支持 钉钉报警 public static void doAlarm
public class AlarmFacade {
public static void doAlarm(FailureResult failureResult){
new AlarmService().doAlarm(failureResult);
}
}
这里先支持钉钉报警。具体的钉钉报警在放在了AlarmService()里,调用一下就可以了。
我们将钉钉报警单独写了一个类,抽了出来,没有放在这里具体实现,是未来将来扩展用,将来要是其他地方用到了钉钉报警,可以直接调用钉钉报警的方法。
2、具体钉钉的报警方法AlarmService
package com.example.autoapi.alarm.service;
import com.example.autoapi.annotation.CaseDesc;
import com.example.autoapi.annotation.CaseTitle;
import com.example.autoapi.annotation.CheckPoint;
import com.example.autoapi.model.FailureResult;
import com.example.autoapi.template.TemplateFacade;
import com.example.autoapi.util.ReflectUtils;
import com.google.common.base.Joiner;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// 钉钉报警
public class AlarmService {
public void doAlarm(FailureResult failureResult){
String className = failureResult.getClassName();
String methodName = failureResult.getMethodName();
Method method = ReflectUtils.getMethod(className,methodName);
String title = null;
String desc = null;
String owner = null;
List<String> cps = null;
String caseId = className+"#"+methodName;
String failureMsg = failureResult.getThrowable().getMessage();
if(method.isAnnotationPresent(CaseTitle.class)){
// 判断case的标题是否存在
title = method.getAnnotation(CaseTitle.class).value();
}
desc = method.getAnnotation(CaseDesc.class).desc();
owner = method.getAnnotation(CaseDesc.class).owner();
CheckPoint[] checkPoints = method.getAnnotationsByType(CheckPoint.class);
cps= Arrays.stream(checkPoints).map(checkPoint -> checkPoint.value()).collect(Collectors.toList());
Map<String,Object> map = new HashMap<>();
map.put("case_title",title);
map.put("case_desc",desc);
map.put("case_id",caseId);
map.put("case_owner",owner);
map.put("case_cps", Joiner.on(",").join(cps));
map.put("failure_msg",failureMsg);
String template = TemplateFacade.replaceTemplate("default_alarm_template", map);
System.out.println("AlarmService.doAlarm");
}
}
1、获取报警case的信息,比如用例的标题、id、owner、描述
2、将拿到的这些信息,按照一个模版,组成一个字符串
String template = TemplateFacade.replaceTemplate("default_alarm_template", map);
比如将报错信息,按照以下这个模版,组成一个字符串
-----------用例运行失败:报警-----------
用例标题:${case_title}
用例描述:${case_desc}
用例 ID:${case_id}
owner:${case_owner}
检查点:${case_cps}
失败原因:${failure_msg}
具体将信息按照模版组装成一个字符串,的设计,如下
a、首先设计2个报告模版。一个是钉钉case的报警模版,一个是报告模版。
因为我们将来要发送报告和报警,这两个都要按照我们模版的格式来组装并发送。
default_alarm_template
-----------用例运行失败:报警-----------
用例标题:${case_title}
用例描述:${case_desc}
用例 ID:${case_id}
owner:${case_owner}
检查点:${case_cp}
失败原因:${failure_msg}
模板这里,有三个类
TemplateFactory
TemplateFactory 模板工厂,作用就是根据文件名称,来读取模版文件里的内容。
package com.example.autoapi.template;
import com.google.common.io.Resources;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class TemplateFactory {
// 所有模版存放的文件夹名称
public final static String TEMPLATE_ROOT_PATH = "templates";
private Map<String,String> templateMap;
// 构造器,私有,不能被new
private TemplateFactory(){
templateMap = initTemplateMap();
}
private Map<String,String> initTemplateMap(){
// 读取templates文件夹下所有的模版文件
// 将文件名与文件内容构建成key-value 的map,方便取数据
// 获取存放模版文件夹的路径
String rootPath = Resources.getResource(TEMPLATE_ROOT_PATH).getPath();
File file = new File(rootPath);
// 获取文件夹内所有是文件的文件
File[] files = file.listFiles(f -> f.isFile()); // 判断是否文件,是则加入数组
// 将文件名与文件内容组成map
return Arrays.stream(files).collect(Collectors.toMap(f->f.getName(),f->getData(f)));
}
// 读取文件里的内容,并返回字符串
private String getData(File f){
try {
return FileUtils.readFileToString(f,"UTF-8");
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
public String getTemplate(String key){
return templateMap.get(key);
}
// ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
// 但是不会把内部类的属性加载出来
private static class ClassHolder{
// 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
// 这里不是调用,是类加载,是成员变量
private static final TemplateFactory holder =new TemplateFactory();
}
public static TemplateFactory of(){//第一次调用getInstance()的时候赋值
return ClassHolder.holder;
}
}
1、我们这里用的是单例模式,一次性把templates文件夹下所有的text文件都读取出来了。
这里不仅仅是报警的模版,还包含报告的模版,将来还有可能是更多的模版。
使用单例模式,就是一次性全部读完,以后再用到模版相关的直接,取就可以了,不用反复读。
2、将文件名与文件内容 存放在了map里,key是文件的名字,value是文件内容,这样通过输入文件名key就可以拿到模版内容value
TemplateService
TemplateService ,拿到模版了,就把case报警信息,替换掉模版里面的变量。
得到一个真实可用的报警String
package com.example.autoapi.template;
import com.example.autoapi.util.RequireUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
public class TemplateService {
public String getTemplateService(String key){
RequireUtil.requireNotNullOrEmpty(key,"key is not empty");
return TemplateFactory.of().getTemplate(key);
}
public String replaceTemplate(String key, Map<String,Object> map){
// 1、读取模版文件里的String template
// 2、数据传进来是一个map
// 遍历map,拿到map的每一个entry,取里面的key和value
// 3、开始替换,将template 里面的getTag(entry.getKey()) 替换成map的value
String template = getTemplateService(key);
for(Map.Entry<String,Object> entry: map.entrySet()){
template =StringUtils.replace(template,getTag(entry.getKey()),String.valueOf(entry.getValue()));
}
return template;
}
private String getTag(String key){
// 将case_title 转换成 ${case_title}
return String.format("${%s}",key);
}
}
TemplateFacade
package com.example.autoapi.template;
import java.util.Map;
public class TemplateFacade {
public static String getTemplate(String key){
return new TemplateService().getTemplateService(key);
}
public static String replaceTemplate(String key, Map<String,Object> map){
return new TemplateService().replaceTemplate(key,map);
}
}
一些其他工具类
ReflectUtils
package com.example.autoapi.util;
import java.lang.reflect.Method;
// 这个工具,就是反射工具
// 通过各种方式,反射,拿到实例
// 我们这里只是多处理了异常,这样调用的时候,就不用处理异常了
public class ReflectUtils {
// 反射,通过class 拿到实例
public static <T> T newInstance(Class<T> clszz){
try {
return clszz.newInstance();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
// 反射,通过String的全类名className拿到这个类
// 再通过方法名,拿到这个方法
public static Method getMethod(String className,String methodName){
try {
Class<?> cls = Class.forName(className);
return cls.getMethod(methodName);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
四、完善单个case报警 AlarmExtension
我们以上讲的是批量报警的逻辑,现在完善单个的。
package com.example.autoapi.extension;
import com.example.autoapi.annotation.DingTalkAlarm;
import com.example.autoapi.model.FailureResult;
import com.example.autoapi.util.ReflectUtils;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;
//TestExecutionExceptionHandler 框架提供的能力,测试执行失败,触发的回调
public class AlarmExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable {
Method testMethod = extensionContext.getRequiredTestMethod();
Class<?> testClass = extensionContext.getRequiredTestClass();
Class<?>[] parameterTypes = testMethod.getParameterTypes();
System.out.println("AlarmExtension.handleTestExecutionException");
String params = Arrays.stream(parameterTypes).map(cls->cls.getName()).collect(Collectors.joining(","));
FailureResult failureResult = FailureResult.builder()
.className(testClass.getName())
.methodName(testMethod.getName())
.parameterTypes(params)
.throwable(throwable)
.build();
DingTalkAlarm dingTalkAlarm = testMethod.getAnnotation(DingTalkAlarm.class);
ReflectUtils.newInstance(dingTalkAlarm.alarmCallBack()).postAlarm(failureResult);
}
}
1、收集case的报错信息,封装成类
这里将信息封装成类,单个与批量有一些不同。
2、发送报警