0
点赞
收藏
分享

微信扫一扫

六、junit接口自动化框架-单个&批量case触发报警的逻辑


目录

​​背景:​​

​​场景一:​​

​​场景二:​​

​​一、单独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执行失败后,就会触发执行这个扩展类。

(这里先简单打印信息,不做具体的报警处理)

整体流程:

六、junit接口自动化框架-单个&批量case触发报警的逻辑_自定义

------------------------------------------------------------------------------------------------------------------------

二、给运行入口打报警标签,批量进行报警

这是大概的整个流程

六、junit接口自动化框架-单个&批量case触发报警的逻辑_java_02

 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的报警模版,一个是报告模版。

因为我们将来要发送报告和报警,这两个都要按照我们模版的格式来组装并发送。

六、junit接口自动化框架-单个&批量case触发报警的逻辑_自定义_03

default_alarm_template

-----------用例运行失败:报警-----------
用例标题:${case_title}
用例描述:${case_desc}
用例 ID:${case_id}
owner:${case_owner}
检查点:${case_cp}
失败原因:${failure_msg}

 模板这里,有三个类

六、junit接口自动化框架-单个&批量case触发报警的逻辑_自动化_04

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、发送报警


举报

相关推荐

0 条评论