纯java手写打造方法级白盒测试框架

tech2022-09-29  67

纯java手写打造方法级白盒测试框架

​ 我们在做服务端测试时,经常使用的自动化测试框架或平台大多通过restful风格使用http协议接入系统,例如常见的Jmeter、LoadRunner、Postman等,还有不常用或付费的工具如TestRail、katalon、soapUI等,很少见到切入到方法层实现更细颗粒度的工具。对此,本文将封装一款切入到方法层并实现可视化操作的白盒测试框架,突破http应用层壁垒,协助测试人员实现方法级别的测试。

一、使用技术评估

1、开发语言

​ 本框架使用java作为开发语言,大版本为jdk1.8,涉及到的关键技术有java反射、字节码编程、自定义注解、Stream流(jdk8)等。

2、目标方法标识

​ 如果测试人员想测试某个或某组方法,可行方式是在代码方法上打上标识,方便项目运行期自动收集这些方法,本框架使用java自定义注解实现对目标方法的标识。

3、可视化展示

​ 本框架使用模板引擎thymeleaf+springboot提供测试人员操作界面的可视化展示。

二、设计思路简述

1、定义捕捉标记,便于收集目标方法

​ 为了方便测试人员在目标方法上标注,需要新建一个自定义注解@White,主要包含两个信息:一是目标方法的测试备注,二是方法的执行环境配置(判断目标方法是否使用spring容器环境运行)。

@Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface White { /** * 方法标注名 * * @return */ String value() default ""; /** * 方法是否来自或使用Spring容器对象(默认不使用) * * @return */ boolean fromIOC() default false; }

2、定义接口,方法捕捉器与方法执行器

​ 我们需要定义一个方法捕捉器,用于在项目运行期收集捕捉标注为@White的方法,再定义一个方法执行器,用于测试时执行相关方法。

​ 捕捉器接口,重载了一个含有泛型参数的方法,方便传入的不同的捕获源,如包名、包名组或ioc容器对象。

public interface Catcher<T> { /** * 执行捕捉操作 */ void doCatch() throws Exception; /** * 执行捕捉操作(传参数) */ void doCatch(T t) throws Exception; }

​ 执行器接口,重载了一个含参方法,ActElement用于传入目标方法的测试参数,同时扩展了一个执行次数,用于执行压力测试。

public interface Actuator { /** * 执行目标方法 * * @return * @throws Exception */ void doWhiteMethod() throws Exception; /** * 执行目标方法 * * @param element * @throws Exception */ void doWhiteMethod(ActElement element) throws Exception; } @Data @NoArgsConstructor //无参构造 @AllArgsConstructor //有参构造 public class ActElement { /** * 执行次数 */ private int actNums; /** * 测试参数 */ private String params; }

​ 抽象处理器AbstractProcessor,实现捕获器和执行器(利用适配器模式,继承该方法后可不必实现所有接口方法,保持代码清爽简洁)

public abstract class AbstractProcessor implements Actuator, Catcher { @Override public void doWhiteMethod() throws Exception { } @Override public void doWhiteMethod(ActElement element) throws Exception { } @Override public void doCatch() throws Exception { } @Override public void doCatch(Object o) throws Exception { } }

3、封装方法工厂

​ 首先定义WhiteMethod类,作为白盒测试的方法主体,它继承了AbstractProcessor类,并提供了Actuator方法执行器接口的实现。

@Data @NoArgsConstructor //无参构造 @AllArgsConstructor //有参构造 public class WhiteMethod extends AbstractProcessor { /** * 类名 */ private String className; /** * 注解注释 */ private String notes; /** * 反射class */ private Class<?> clazz; /** * 反射方法 */ private Method method; /** * 方法是否来自或使用Spring容器对象 */ private boolean fromIOC; /** * spring容器中的bean对象 */ private Object bean; @Override public void doWhiteMethod(ActElement element) throws Exception { excuteMethod(element.getActNums(), element.getParams()); } /** * 反射执行方法 * * @param params 输入参数 */ public void excuteMethod(String params) throws Exception { System.out.println("======= Test Staring ======>> [class/bean:" + className + ", method:" + method.getName() + "]"); //如果测试人员未输入测试参数,使用默认值生成参数组 Object[] args = (params == null || "".equals(params)) ? AutoSourceUtil.getSampleParams(method) : AutoSourceUtil.getRealParams(method, params); //是否使用容器对象执行方法 if (fromIOC) { AutoSourceUtil.doMethod(clazz, method, args, bean); } else { AutoSourceUtil.doMethod(clazz, method, args, null); } } /** * 反射执行方法(多次) * * @param params 输入参数 * @param times 执行次数 */ public void excuteMethod(int times, String params) throws Exception { while (times > 0) { excuteMethod(params); times--; } } }

​ 我们通过一个核心类MethodsFactory,来封装白盒测试的方法工厂,内部维护了一个map格式的方法池。MethodsFactory继承自AbstractProcessor,实现了捕捉器接口的doCatch(Object object)方法,object扩展了三种入参,分别是包路径、包路径数组、ioc容器,根据不同入参类型通过initFactory()方法来实现工厂的初始化。

/** * 白盒测试的方法池 */ public static final Map<String, WhiteMethod> whiteMethods = new ConcurrentHashMap<>(); /** * 实现白盒测试方法捕捉器 * * @param object 捕获入口,包路径或IOC容器 * @throws Exception */ @Override public void doCatch(Object object) throws Exception { if (object instanceof String) { initFactory((String) object); } else if (object instanceof String[]) { initFactory((String[]) object); } else if (object instanceof ApplicationContext) { initFactory((ApplicationContext) object); } } /** * 通过包名初始化工厂 * * @param packageNames 扫描的包名组 * @throws Exception */ public void initFactory(String[] packageNames) throws Exception { initFactory(packageNames, null); } /** * 通过包名初始化工厂(二) * * @param packageName 扫描的包名 * @throws Exception */ public void initFactory(String packageName) throws Exception { String[] packageNames = new String[]{packageName}; initFactory(packageNames, null); } /** * 通过容器初始化工厂 * * @param context IOC容器 * @throws Exception */ public void initFactory(ApplicationContext context) throws Exception { initFactory(null, context); } /** * 初始化工厂 * * @param packageNames 扫描的包名 * @param context IOC容器 * @throws Exception */ public void initFactory(String[] packageNames, ApplicationContext context) throws Exception { //初始化普通方法入池 if (packageNames != null) { Arrays.asList(packageNames).stream().forEach(name -> initWhiteMethods(name)); } //初始化容器相关方法入池 if (context != null) { initWhiteMethods(context); } }

​ 初始化工厂,这里重载了initWhiteMethods()方法,区分使用包名还是容器去扫描捕捉对应的目标方法。其中,通过包名的方式扫描,使用工具类PackageUtil的getAllTargets(String packageName)方法,获取所有packageName下类名集合并遍历,收集类中匹配White注解的方法,而通过ioc容器的方式扫描,则是通过spring-beans包的ListableBeanFactory接口(继承自BeanFactory接口,即spring容器的bean工厂)的getBeanDefinitionNames()方法获取所有的bean集合,再执行遍历收集。

/** * 通过包名路径初始化方法池 * * @param packageName */ public void initWhiteMethods(String packageName) { List<String> list = PackageUtil.getAllTargets(packageName); if (list.size() == 0) { return; } list.stream().forEach(className -> { try { Class<?> clazz = Class.forName(className); Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(White.class)) { White white = method.getAnnotation(White.class); if (!white.fromIOC()) {//只收集非容器关联的方法 whiteMethods.put("package_key_" + className + "_" + method.getName(), new WhiteMethod(className, white.value(), clazz, method, white.fromIOC(), null)); } } } } catch (Exception e) { e.printStackTrace(); } }); } /** * 通过spring容器初始化方法池 * * @param applicationContext */ public void initWhiteMethods(ApplicationContext applicationContext) { String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); Arrays.asList(beanDefinitionNames).stream().forEach(beanDefinitionName -> { try { Class<?> clazz = applicationContext.getType(beanDefinitionName); Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(White.class)) { White white = method.getAnnotation(White.class); if (white.fromIOC()) { whiteMethods.put("spring_key_" + beanDefinitionName + "_" + method.getName(), new WhiteMethod(beanDefinitionName, method.getAnnotation(White.class).value(), clazz, method, white.fromIOC(), applicationContext.getBean(beanDefinitionName))); } } } } catch (Exception e) { e.printStackTrace(); } }); }

4、方法执行

​ 我们通过java反射执行目标测试的方法,invoke(Object obj, Object… args),必要元素为方法所属对象(obj)、参数(args)。

(1)新建方法所属对象

​ 一般使用class对象的newInstance()方法新建对象,但是当方法来自或使用Spring容器时,改为使用BeanFactory接口的getBean(String var1)方法获取bean对象,如果强行使用newInstance()新建对象,有可能导致获取对象某些属性时出现空指针异常。

(2)模拟/获取拼接方法参数

​ 如果要进行测试的方法是有参的,但是测试人员没有输入参数,或者执行批量压力测试不方便输入动态参数时,我们可以通过参数的类型对参数数组进行赋初始化值,如果测试人员有输入参数(以String方式,逗号分隔),则进行动态解析成对应的args参数数组。AutoSourceUtil封装了具体拼接与执行方式,其中参数有可能是基本类型,这时候要使用枚举类BaseJavaType做一下特殊处理。

public class AutoSourceUtil { /** * 返回反射对应的样例初始化值 * * @param clazz 反射对象 * @return * @throws Exception */ public static Object getSampleValue(Class clazz) throws Exception { if (clazz == BaseJavaType.BYTE.getVal()) return 0; if (clazz == BaseJavaType.CHAR.getVal()) return 0; if (clazz == BaseJavaType.SHORT.getVal()) return 0; if (clazz == BaseJavaType.INT.getVal()) return 0; if (clazz == BaseJavaType.LONG.getVal()) return 0; if (clazz == BaseJavaType.DOUBLE.getVal()) return 0; if (clazz == BaseJavaType.FLOAT.getVal()) return 0; if (clazz == BaseJavaType.BOOLEAN.getVal()) return false; else return clazz.newInstance(); } /** * 返回样例测试对应的基本类型值或对象 * * @param clazz 反射对象 * @param param 测试方法字符串 * @return */ public static Object getRealValue(Class clazz, String param) { if (clazz == BaseJavaType.BYTE.getVal()) return Byte.parseByte(param); if (clazz == BaseJavaType.CHAR.getVal()) return param.charAt(0); if (clazz == BaseJavaType.SHORT.getVal()) return Short.parseShort(param); if (clazz == BaseJavaType.INT.getVal()) return Integer.parseInt(param); if (clazz == BaseJavaType.LONG.getVal()) return Long.parseLong(param); if (clazz == BaseJavaType.DOUBLE.getVal()) return Double.parseDouble(param); if (clazz == BaseJavaType.FLOAT.getVal()) return Float.parseFloat(param); if (clazz == BaseJavaType.BOOLEAN.getVal()) return Boolean.parseBoolean(param); else return clazz.cast(param); } /** * 反射调用方法 * * @param clazz 反射class对象 * @param method 反射方法 * @param args 需要执行的方法参数 * @param object 方法对象,设置为null时会使用clazz.newInstance()新建对象 * @return * @throws Exception */ public static Object doMethod(Class<?> clazz, Method method, Object[] args, Object object) throws Exception { if (method.getParameterTypes().length == 0) { //无参方法,args置为空 args = null; } return (object == null) ? method.invoke(clazz.newInstance(), args) : method.invoke(object, args); } /** * 对无测试参数的有参方法,构造对应格式的初始入参数组,各参数使用空白默认值 * * @param method 反射的方法 * @return * @throws Exception */ public static Object[] getSampleParams(Method method) throws Exception { //获得一个方法参数数组 Class[] paramTypes = method.getParameterTypes(); //拼接动态参数 Object[] args = new Object[paramTypes.length]; for (int i = 0, j = paramTypes.length; i < j; i++) { args[i] = getSampleValue(paramTypes[i]); } return args; } /** * 根据测试人员输入的参数拼接成对应格式的可执行参数组 * * @param method 反射的方法 * @param params 手动输入的参数 * @return * @throws Exception */ public static Object[] getRealParams(Method method, String params) throws Exception { String[] stringParams = params.split(","); Class[] paramTypes = method.getParameterTypes(); if (paramTypes.length != stringParams.length) { throw new WhiteException("测试参数输入有误!"); } //拼接动态参数 Object[] args = new Object[paramTypes.length]; for (int i = 0, j = paramTypes.length; i < j; i++) { //基本类型参数处理, args[i] = getRealValue(paramTypes[i], stringParams[i]); } return args; } } public enum BaseJavaType { BYTE("byte", byte.class), CHAR("char", char.class), SHORT("short", short.class), INT("int", int.class), LONG("long", long.class), DOUBLE("double", double.class), FLOAT("float", float.class), BOOLEAN("boolean", boolean.class); BaseJavaType(String name, Class clazz) { this.name = name; this.clazz = clazz; } public Class getVal() { return clazz; } //基本变量类型 private String name; //基本变量类型对应的Class private Class clazz; }

三、使用介绍

​ 使用时继承MethodsFactory类即可使用方法捕捉器和执行器。

1、原生jvm虚拟机环境使用

​ 我们在需要测试的类Demo上的各个方法上加上@White注解。

package com.whiteBox; import com.whiteBox.core.anno.White; public class Demo { private int result; @White("无参方法") public int getResult() { System.out.println("getResult() has be done"); return result; } public void setResult(int result) { this.result = result; } @White("有参(一个参数)") public void one(String one) { System.out.println("method_one:" + one); } @White("有参(两个参数)") public void two(String two, String twos) { System.out.println("method_two:" + two + twos); } @White("有参(不同类型参数)") public void three(String two, int hello) { System.out.println(two + "method_three:" + hello); } }

​ DemoTest继承MethodsFactory,调用捕捉器扫描"com.whiteBox"包名下的所有标注了@White的方法,再调用MethodsFactory实现的执行器doWhiteMethod(),它可以将所有扫描收到到的方法执行一遍,如果是有参数方法,会根据类型创建初始参数。

public class DemoTest extends MethodsFactory { public static void main(String[] args) throws Exception { DemoTest demoTest = new DemoTest(); //捕捉器捕捉相应包内标记的方法 demoTest.doCatch("com.whiteBox"); //执行白盒测试 demoTest.doWhiteMethod(); } }

​ 控制台响应结果如下:可以看到,被@White标注的方法被成功执行。

======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:two] method_two: ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:one] method_one: ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] method_three:0 ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:getResult] getResult() has be done

2、spring容器环境中使用

(1)spring样例环境搭建

​ 我们使用springboot+thymeleaf搭建一个简单的用户增删改查demo,maven构建项目,pom添加依赖:

<!-- thymeleaf引擎依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>jquery</artifactId> <version>2.0.3</version> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>bootstrap</artifactId> <version>3.0.3</version> </dependency> <!--lombok组件使用--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>

​ 后端按照经典MVC模式设计,其中dao层和service层各方法加上了@White注解,代表该方法将被白盒测试框架收集。

@Data @NoArgsConstructor //无参构造 @AllArgsConstructor //有参构造 public class User { private int userId; private String userName; private String sex; private String desc; } @Component public class UserDao { private static List<User> locallist = new ArrayList<>(); private static AtomicInteger atomicInteger = new AtomicInteger(1); static { User u = new User(0, "元始天尊", "男", "最开始的人"); locallist.add(u); } @White(value = "dao层-查询用户", fromIOC = true) public User getUser(int id) { List<User> users = locallist.stream().filter(u -> u.getUserId() == id).collect(Collectors.toList()); return users.get(0); } @White(value = "dao层-用户列表", fromIOC = true) public List<User> list() { return locallist; } @White(value = "dao层-新增用户", fromIOC = true) public void add(User user) { user.setUserId(atomicInteger.getAndIncrement()); locallist.add(user); } @White(value = "dao层-修改用户", fromIOC = true) public void edit(User user) { locallist.removeIf(u -> u.getUserId() == user.getUserId()); locallist.add(user); } @White(value = "dao层-删除用户", fromIOC = true) public void delete(int id) { locallist.removeIf(u -> u.getUserId() == id); } } @Service public class UserService { @Autowired private UserDao userDao; @White(value = "service层-查询用户", fromIOC = true) public User getUser(int id) { return userDao.getUser(id); } @White(value = "service层-用户列表", fromIOC = true) public List<User> list() { return userDao.list(); } @White(value = "service层-新增用户", fromIOC = true) public void add(User user) { userDao.add(user); } @White(value = "service层-修改用户", fromIOC = true) public void edit(User user) { userDao.edit(user); } @White(value = "service层-删除用户", fromIOC = true) public void delete(int id) { userDao.delete(id); } } @Controller public class UserController { @Autowired private UserService userService; @RequestMapping("/") public String index() { return "redirect:/list"; } @RequestMapping("/list") public String list(Model model) { model.addAttribute("users", userService.list()); return "user/list"; } @RequestMapping("/toAdd") public String toAdd(Model model) { User user = new User(); model.addAttribute("user", user); return "user/userAdd"; } @RequestMapping("/add") public String add(User user) { userService.add(user); return "redirect:/list"; } @RequestMapping("/toEdit") public String toEdit(Model model, int id) { model.addAttribute("user", userService.getUser(id)); return "user/userEdit"; } @RequestMapping(value = "/edit") public String edit(User user) { userService.edit(user); return "redirect:/list"; } @RequestMapping("/delete") public String delete(int id) { userService.delete(id); return "redirect:/list"; } }

​ 使用thymeleaf模板引擎编写表单页面,用户列表:list.html

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>用户列表</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> </head> <body class="container"> <br/> <h3>用户列表</h3> <br/><br/> <div class="with:80%"> <table class="table table-hover"> <thead> <tr> <th>编号</th> <th>用户名</th> <th>性别</th> <th>描述</th> <th>修改</th> <th>删除</th> </tr> </thead> <tbody> <tr th:each="user : ${users}"> <th scope="row" th:text="${user.userId}">1</th> <td th:text="${user.userName}">neo</td> <td th:text="${user.sex}">1</td> <td th:text="${user.desc}">6</td> <td><a th:href="@{/toEdit(id=${user.userId})}">编辑</a></td> <td><a th:href="@{/delete(id=${user.userId})}">删除</a></td> </tr> </tbody> </table> </div> <div class="form-group"> <div class="col-sm-2 control-label"> <a href="/toAdd" th:href="@{/toAdd}" class="btn btn-info">新增</a> </div> </div> </body>

​ 新增页面userAdd.html

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>添加用户</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> </head> <body class="container"> <br/> <h1>增加用户</h1> <br/><br/> <div class="with:80%"> <form class="form-horizontal" th:action="@{/add}" th:object="${user}" method="post"> <input type="hidden" name="userId" th:value="*{userId}" /> <div class="form-group"> <label for="userName" class="col-sm-2 control-label">userName</label> <div class="col-sm-10"> <input type="text" class="form-control" name="userName" id="userName" placeholder="userName"/> </div> </div> <div class="form-group"> <label for="sex" class="col-sm-2 control-label">age</label> <div class="col-sm-10"> <input type="text" class="form-control" name="sex" id="sex" placeholder="sex"/> </div> </div> <div class="form-group"> <label for="desc" class="col-sm-2 control-label">desc</label> <div class="col-sm-10"> <input type="text" class="form-control" name="desc" id="desc" placeholder="desc"/> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" value="提交" class="btn btn-info" /> &nbsp; &nbsp; &nbsp; <a href="/toAdd" th:href="@{/list}" class="btn btn-info">返回</a> </div> </div> </form> </div> </body> </html>

​ 编辑页面userEdit.html

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>修改用户</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> </head> <body class="container"> <br/> <h1>修改用户</h1> <br/><br/> <div class="with:80%"> <form class="form-horizontal" th:action="@{/edit}" th:object="${user}" method="post"> <input type="hidden" name="userId" th:value="*{userId}"/> <div class="form-group"> <label for="userName" class="col-sm-2 control-label">userName</label> <div class="col-sm-10"> <input type="text" class="form-control" name="userName" id="userName" th:value="*{userName}" placeholder="userName"/> </div> </div> <div class="form-group"> <label for="sex" class="col-sm-2 control-label">age</label> <div class="col-sm-10"> <input type="text" class="form-control" name="sex" id="sex" th:value="*{sex}" placeholder="sex"/> </div> </div> <div class="form-group"> <label for="desc" class="col-sm-2 control-label">desc</label> <div class="col-sm-10"> <input type="text" class="form-control" name="desc" id="desc" th:value="*{desc}" placeholder="desc"/> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" value="提交" class="btn btn-info"/> &nbsp; &nbsp; &nbsp; <a href="/toAdd" th:href="@{/list}" class="btn btn-info">返回</a> </div> </div> </form> </div> </body> </html>

​ 本地启动容器,访问http://localhost:8080/list,即可进行简单的user增删改查,此处不赘述。

(2)容器下执行捕捉器收集白盒测试方法

​ 我们可以在spring容器启动后使用方法捕捉器收集目标方法,在管理平台界面进行单个方法的分析与测试。这里定义一个WhiteHandler,继承MethodsFactory,实现ApplicatonContextAware接口,在setApplicationContext()方法中获取BeanFactory的实例applicationContext,同时对包"com.whiteBox"和spring容器进行扫描收集。

@Component public class WhiteHandler extends MethodsFactory implements ApplicationContextAware { /** * 重写setApplicationContext方法,获取IOC容器:ApplicationContext实例 * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { try { //捕捉该包下的标注方法 doCatch("com.whiteBox"); //捕捉spring容器环境下相关标注方法 doCatch(applicationContext); } catch (Exception e) { e.printStackTrace(); } } }
(3)容器下使用方法执行器进行测试

​ 为方便图形化测试,新建一个WhiteController,提供白盒方法的查询和执行处理。前面提到WhiteMethod继承了AbstractProcessor并实现了doWhiteMethod方法,可以通过它对单个方法进行测试执行。

@Controller @RequestMapping("white") public class WhiteController { /** * 获取工厂里的方法池,并以List形式输出 * @return */ private List<WhiteMethod> getMethods(){ return MethodsFactory.whiteMethods.values().stream() .collect(Collectors.toList()); } /** * 方法列表,列出所有标注的方法 * * @param model * @return */ @RequestMapping("/list") public String list(Model model) { model.addAttribute("methods", getMethods()); return "white/whiteList"; } /** * 跳转至方法详情 * * @param model * @param index * @return */ @RequestMapping("/toExecute") public String toExecute(Model model, int index) { WhiteMethod whiteMethod = getMethods().get(index); model.addAttribute("index", index); model.addAttribute("whiteMethod", whiteMethod); return "white/doExecute"; } /** * 执行测试方法 * * @param index * @param times * @param params * @return * @throws Exception */ @RequestMapping("/execute") public String execute(int index, int times, String params) throws Exception { WhiteMethod whiteMethod = getMethods().get(index); ActElement element = new ActElement(times, params); whiteMethod.doWhiteMethod(element); return "redirect:/white/list"; } }

​ 前端页面whiteList.html

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>白盒测试方法列表</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> </head> <body class="container"> <br/> <h3>方法列表</h3> <br/><br/> <div class="with:80%"> <table class="table table-hover"> <thead> <tr> <th>序号</th> <th>类名/Bean名</th> <th>方法名</th> <th>标注</th> <th>容器</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="whiteMethod : ${methods}"> <th scope="row" width="60px" th:text="${methods.indexOf(whiteMethod)+1}">1</th> <td th:text="${whiteMethod.className}"></td> <td th:text="${whiteMethod.method}"></td> <td width="200px" th:text="${whiteMethod.notes}">1</td> <td width="50px" th:text="${whiteMethod.fromIOC}">1</td> <td width="60px"><a th:href="@{/white/toExecute(index=${methods.indexOf(whiteMethod)})}">执行</a></td> </tr> </tbody> </table> </div> </body> </html>

​ 方法执行页面doExecute.html

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>白盒测试方法列表</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> </head> <body class="container"> <br/> <h3>方法列表</h3> <br/><br/> <div class="with:80%"> <table class="table table-hover"> <thead> <tr> <th>序号</th> <th>类名/Bean名</th> <th>方法名</th> <th>标注</th> <th>容器</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="whiteMethod : ${methods}"> <th scope="row" width="60px" th:text="${methods.indexOf(whiteMethod)+1}">1</th> <td th:text="${whiteMethod.className}"></td> <td th:text="${whiteMethod.method}"></td> <td width="200px" th:text="${whiteMethod.notes}">1</td> <td width="50px" th:text="${whiteMethod.fromIOC}">1</td> <td width="60px"><a th:href="@{/white/toExecute(index=${methods.indexOf(whiteMethod)})}">执行</a></td> </tr> </tbody> </table> </div> </body> </html>

​ 启动容器后,访问http://localhost:8080/white/list,即可看到所有被标注的方法详情,其中容器,true代表使用spring容器环境的对象执行方法。

a.测试普通方法:

​ 点击第7条,输入测试参数:“你好,2020”,次数5。

​ 点击提交,控制台打印的测试日志如下,可以看到该方法执行了5次:

======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] 你好method_three:2020 ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] 你好method_three:2020 ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] 你好method_three:2020 ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] 你好method_three:2020 ======= Test Staring ======>> [class/bean:com.whiteBox.Demo, method:three] 你好method_three:2020
b.测试容器方法

​ 在容器方法的注解上fromIOC属性要为true,我们点击列表第5条,测试service层的add方法,输入次数为2,点击提交。

​ 控制台日志,可以看到userService的新增方法执行了2次

======= Test Staring ======>> [class/bean:userService, method:add] ======= Test Staring ======>> [class/bean:userService, method:add]

​ 再访问用户列表http://localhost:8080/list,即可看到新增了id为1和2两个用户,测试service层新增方法成功。

​ 下面我们测试下dao层的删除方法,回到http://localhost:8080/white/list方法列表,点击第12条执行,输入测试参数:2,次数:1,点击提交,这时删除将id为2的用户。

​ 控制台日志,可以看到userDao的删除方法执行了1次

======= Test Staring ======>> [class/bean:userDao, method:delete]

​ 重新查看用户列表,发现id为2的用户已经被删除,测试dao层方法成功执行。

四、总结与思考

​ 本框架尝试通过自定义注解标识目标方法并项目运行期自动收集相关方法,封装测试参数和测试次数入口,达到辅助测试人员进行白盒或压力测试的目标。代码尽可能的兼容到了复杂运行环境和测试需求,但是难免有一些不足的地方,例如执行海量次数压力请求设置成异步、进一步集成序列化组件实现跨语言支持等。最后给大家留下一个思考,如果业务代码的Service层方法添加基于 @Transactional 的声明式事务管理,通过本框架是否能实现对事务的支持呢?

代码下载地址:https://github.com/kaccnGit/whiteframework

最新回复(0)