SpringBoot环境下junit单元测试速度优化方式

目录
  • 1、提高单元测试效率
    • 背景
  • 2、单元测试如何执行
    • 补充说明
  • 3、项目中使用
  • 4、优化单测思路
    • 思路
  • 5、实现方式
  • 6、编码实现
    • 6.1 Jetty作为服务启动
    • 6.2 Tomcat作为容器启动

1、提高单元测试效率

背景

在项目提测前,自己需要对代码逻辑进行验证,所以单元测试必不可少。

但是现在的java项目几乎都是基于SpringBoot系列开发的,所以在进行单元测试时,执行一个测试类就要启动springboot项目,加载上下文数据,每次执行一次测试都要再重新加载上下文环境,这样就会很麻烦,浪费时间;在一次项目中,我们使用自己的技术框架进行开发,每次单元测试时都要初始化很多数据(例如根据数据模型建立表,加载依赖其它模块的类),这样导致每一次单元测试时都会花3-5分钟时间(MacOs 四核Intel Core i5 内存:16g),所以很有必要优化单元测试效率,节约开发时间。

2、单元测试如何执行

首先要优化单元测试,那要知道单元测试是怎样执行的

引入相关测试的maven依赖,例如junit,之后在测试方法加上@Test注解即可,在springboot项目测试中还需要在测试类加上@RunWith注解 然后允许需要测试的方法即可

补充说明

  • @RunWith 就是一个运行器
  • @RunWith(JUnit4.class) 就是指用JUnit4来运行
  • @RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境
  • @RunWith(Suite.class) 的话就是一套测试集合,
  • @ContextConfiguration Spring整合JUnit4测试时,使用注解引入多个配置文件@RunWith

SpringBoot环境下单元测试一般是加@RunWith(SpringJUnit4ClassRunner.class)注解,SpringJUnit4ClassRunner继承BlockJUnit4ClassRunner类,然后在测试方式时会执行SpringJUnit4ClassRunner类的run方法(重写了BlockJUnit4ClassRunner的run方法),run方法主要是初始化spring环境数据,与执行测试方法

3、项目中使用

在我们项目中,是通过一个RewriteSpringJUnit4ClassRunner类继承SpringJUnit4ClassRunner,然后@RunWith(RewriteSpringJUnit4ClassRunner.class)来初始化我们框架中需要的数据,

RewriteSpringJUnit4ClassRunner里面是通过重写withBefores方法,在withBefores方法中去初始化数据的,之后通过run方法最后代理执行测试方法

4、优化单测思路

通过上面说明,可以知道每次测试一个方法都要初始化springboot环境与加载自己框架的数据,所以有没有一种方式可以只需要初始化 一次数据,就可以反复运行测试的方法呢?

思路

首先每一次单测都需要重新加载数据,跑完一次程序就结束了,所以每次测试方法时都要重新加载数据,

如果只需要启动一次把环境数据都加载了,然后之后都单元测试方法都使用这个环境呢那不就能解决这个问题么。

我们是不是可以搞一个服务器,把基础环境与数据都加载进去,然后每次执行单元测试方法时,通过服务器代理去执行这个方法,不就可以了吗

5、实现方式

首先我们可以用springboot的方式启动一个服务,通常使用的内置tomcat作为服务启,之后暴露一个http接口,入参为需要执行的类和方法,然后通过反射去执行这个方法;还可以通过启动jetty服务,通过jetty提供的handler处理器就可以处理请求,jetty相对于tomcat处理请求更加方便

服务是有了,那怎样将单元测试方法代理给服务器呢?前面提到过,通过@RunWith注入的类,在单元测试方法运行时会执行@RunWith注入的类相应的方法,所以我们可以在@RunWith注入的类里面做文章,拿到测试类与方法,然后通过http访问服务器,然后服务器去代理执行测试方法

6、编码实现

下面将通过两种不同方式实现,以Jetty为服务器启动,与以Tomcat为服务器启动

6.1 Jetty作为服务启动

首先编写服务启动类,并在spring容器准备好后加载我们公司框架相关数据,这里使用jetty作为服务器,下面代码是核心方法

// 只能写在测试目录下,因为写在应用程序目录下在序列化时,找不到测试目录下的类-》InvokeRequest类中的Class<?> testClass反序列化不出来
@SpringBootApplication
@ComponentScan(value = "包路径")
public class DebugRunner {
    public static void main(String... args) {
        SpringApplication.run(DebugRunner.class, args);
        System.out.println("================================success========================");
    }
    @EventListener
    public void onReady(ContextRefreshedEvent event) {
        // 加载框架数据
    }
    @Bean
    public JettyServer jettyServer(ApplicationContext applicationContext) {
        return new JettyServer(port, applicationContext);
    }
}

使用jetty作为服务器,并且注入处理器HttpHandler

public class JettyServer {
    private volatile boolean running = false;
    private Server server;
    private final Integer port;
    private final ApplicationContext applicationContext;
    public JettyServer(Integer port, ApplicationContext applicationContext) {
        this.port = port;
        this.applicationContext = applicationContext;
    }
    @PostConstruct
    public void init() {
        this.startServer();
    }
    private synchronized void startServer() {
        if (!running) {
            try {
                running = true;
                doStart();
            } catch (Throwable e) {
                log.error("Fail to start Jetty Server at port: {}, cause: {}", port, Throwables.getStackTraceAsString(e));
                System.exit(1);
            }
        } else {
            log.error("Jetty Server already started on port: {}", port);
            throw new RuntimeException("Jetty Server already started.");
        }
    }
    private void doStart() throws Throwable {
        if (!assertPort(port)) {
            throw new IllegalArgumentException("Port already in use!");
        }
        server = new Server(port);
        // 注册处理的handler
        server.setHandler(new HttpHandler(applicationContext));
        server.start();
        log.info("Jetty Server started on port: {}", port);
    }
    /**
     * 判断端口是否可用
     *
     * @param port 端口
     * @return 端口是否可用
     */
    private boolean assertPort(int port) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
            return true;
        } catch (IOException e) {
            log.error("An error occur during test server port, cause: {}", Throwables.getStackTraceAsString(e));
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    log.error("An error occur during closing serverSocket, cause: {}", Throwables.getStackTraceAsString(e));
                }
            }
        }
        return false;
    }
}

HttpHandler处理http请求

public class HttpHandler extends AbstractHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    private Map<String, Method> methodMap = new ConcurrentHashMap<>();
    private final ApplicationContext applicationContext;
    public HttpHandler(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    private InvokeRequest readRequest(HttpServletRequest request) throws IOException {
        int contentLength = request.getContentLength();
        ServletInputStream inputStream = request.getInputStream();
        byte[] buffer = new byte[contentLength];
        inputStream.read(buffer, 0, contentLength);
        inputStream.close();
        return objectMapper.readValue(buffer, InvokeRequest.class);
    }
    private void registerBeanOfType(Class<?> type) {
        BeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClassName(type.getName());
        ((DefaultListableBeanFactory) (((GenericApplicationContext) applicationContext).getBeanFactory()))
                .registerBeanDefinition(type.getName(), beanDefinition);
    }
    private Method getMethod(Class clazz, String methodName) {
        String key = clazz.getCanonicalName() + ":" + methodName;
        Method md = null;
        if (methodMap.containsKey(key)) {
            md = methodMap.get(key);
        } else {
            Method[] methods = clazz.getMethods();
            for (Method mth : methods) {
                if (mth.getName().equals(methodName)) {
                    methodMap.putIfAbsent(key, mth);
                    md = mth;
                    break;
                }
            }
        }
        return md;
    }
    private InvokeResult execute(InvokeRequest invokeRequest) {
        Class<?> testClass = invokeRequest.getTestClass();
        Object bean;
        try {
            bean = applicationContext.getBean(testClass.getName());
        } catch (Exception e) {
            registerBeanOfType(testClass);
            bean = applicationContext.getBean(testClass.getName());
        }
        InvokeResult invokeResult = new InvokeResult();
        Method method = getMethod(testClass, invokeRequest.getMethodName());
        try {
            // 远程代理执行
            method.invoke(bean);
            invokeResult.setSuccess(true);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            if (!(e instanceof InvocationTargetException)
                    || !(((InvocationTargetException) e).getTargetException() instanceof AssertionError)) {
                log.error("fail to invoke code, cause: {}", Throwables.getStackTraceAsString(e));
            }
            invokeResult.setSuccess(false);
            // 记录异常类
            InvokeFailedException invokeFailedException = new InvokeFailedException();
            invokeFailedException.setMessage(e.getMessage());
            invokeFailedException.setStackTrace(e.getStackTrace());
            // 由Assert抛出来的错误
            if (e.getCause() instanceof AssertionError) {
                invokeFailedException.setAssertionError((AssertionError) e.getCause());
            }
            invokeResult.setException(invokeFailedException);
        } catch (Exception e) {
            log.error("fail to invoke code, cause: {}", Throwables.getStackTraceAsString(e));
            invokeResult.setSuccess(false);
            InvokeFailedException invokeFailedException = new InvokeFailedException();
            invokeFailedException.setMessage(e.getMessage());
            invokeFailedException.setStackTrace(e.getStackTrace());
        }
        return invokeResult;
    }
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) {
        try {
            InvokeRequest invokeRequest = readRequest(request);
            InvokeResult invokeResult = execute(invokeRequest);
            String result = objectMapper.writeValueAsString(invokeResult);
            response.setHeader("Content-Type", "application/json");
            response.getWriter().write(result);
            response.getWriter().close();
        } catch (Exception e) {
            try {
                response.getWriter().write(Throwables.getStackTraceAsString(e));
                response.getWriter().close();
            } catch (Exception ex) {
                log.error("fail to handle request");
            }
        }
    }
}
public class InvokeRequest implements Serializable {
    private static final long serialVersionUID = 6162519478671749612L;
    /**
     * 测试方法所在的类
     */
    private Class<?> testClass;
    /**
     * 测试的方法名
     */
    private String methodName;
}

编写SpringDelegateRunner继承SpringJUnit4ClassRunner

public class SpringDelegateRunner extends ModifiedSpringJUnit4ClassRunner {
    private ObjectMapper objectMapper = new ObjectMapper();
    private final Class<?> testClass;
    private final Boolean DEBUG_MODE = true;
    public SpringDelegateRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
        this.testClass = clazz;
    }
    /**
     * 递交给远程执行
     *
     * @param method   执行的方法
     * @param notifier Runner通知
     */
    @Override
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        Description description = describe(method);
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
            return;
        }
        InvokeRequest invokeRequest = new InvokeRequest();
        invokeRequest.setTestClass(method.getDeclaringClass());
        invokeRequest.setMethodName(method.getName());
        try {
            notifier.fireTestStarted(description);
            String json = objectMapper.writeValueAsString(invokeRequest);
            // http请求访问服务器
            String body = HttpRequest.post("http://127.0.0.1:" + DebugMaskUtil.getPort()).send(json).body();
            if (StringUtils.isEmpty(body)) {
                notifier.fireTestFailure(new Failure(description, new RuntimeException("远程执行失败")));
            }
            InvokeResult invokeResult = objectMapper.readValue(body, InvokeResult.class);
            Boolean success = invokeResult.getSuccess();
            if (success) {
                notifier.fireTestFinished(description);
            } else {
                InvokeFailedException exception = invokeResult.getException();
                if (exception.getAssertionError() != null) {
                    notifier.fireTestFailure(new Failure(description, exception.getAssertionError()));
                } else {
                    notifier.fireTestFailure(new Failure(description, invokeResult.getException()));
                }
            }
        } catch (Exception e) {
            notifier.fireTestFailure(new Failure(description, e));
        }
        }
    }

6.2 Tomcat作为容器启动

@Slf4j
@Controller
@RequestMapping("junit")
public class TestController {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Autowired
    private ApplicationContext applicationContext;
    private Map<String, Method> methodMap = new ConcurrentHashMap<>();
    @PostMapping("/test")
    public void test(HttpServletRequest request, HttpServletResponse response){
        int contentLength = request.getContentLength();
        ServletInputStream inputStream;
        byte[] buffer = null;
        try {
            inputStream = request.getInputStream();
            buffer = new byte[contentLength];
            inputStream.read(buffer, 0, contentLength);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            InvokeRequest invokeRequest = objectMapper.readValue(buffer, InvokeRequest.class);
//            InvokeRequest invokeRequest = JsonUtil.getObject(new String(buffer),InvokeRequest.class);
            InvokeResult execute = execute(invokeRequest);
            String result = objectMapper.writeValueAsString(execute);
            log.info("==================="+result);
            response.setHeader("Content-Type", "application/json");
            response.getWriter().write(result);
            response.getWriter().close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void registerBeanOfType(Class<?> type) {
        BeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClassName(type.getName());
        ((DefaultListableBeanFactory) (((GenericApplicationContext) applicationContext).getBeanFactory()))
                .registerBeanDefinition(type.getName(), beanDefinition);
    }
    private Method getMethod(Class clazz, String methodName) {
        String key = clazz.getCanonicalName() + ":" + methodName;
        Method md = null;
        if (methodMap.containsKey(key)) {
            md = methodMap.get(key);
        } else {
            Method[] methods = clazz.getMethods();
            for (Method mth : methods) {
                if (mth.getName().equals(methodName)) {
                    methodMap.putIfAbsent(key, mth);
                    md = mth;
                    break;
                }
            }
        }
        return md;
    }
    private InvokeResult execute(InvokeRequest invokeRequest) {
        Class<?> testClass = invokeRequest.getTestClass();
        Object bean;
        try {
            bean = applicationContext.getBean(testClass.getName());
        } catch (Exception e) {
            registerBeanOfType(testClass);
            bean = applicationContext.getBean(testClass.getName());
        }
        InvokeResult invokeResult = new InvokeResult();
        Method method = getMethod(testClass, invokeRequest.getMethodName());
        try {
            method.invoke(bean);
            invokeResult.setSuccess(true);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            if (!(e instanceof InvocationTargetException)
                    || !(((InvocationTargetException) e).getTargetException() instanceof AssertionError)) {
                log.error("fail to invoke code, cause: {}", Throwables.getStackTraceAsString(e));
            }
            invokeResult.setSuccess(false);
            InvokeFailedException invokeFailedException = new InvokeFailedException();
            invokeFailedException.setMessage(e.getMessage());
            invokeFailedException.setStackTrace(e.getStackTrace());
            // 由Assert抛出来的错误
            if (e.getCause() instanceof AssertionError) {
                invokeFailedException.setAssertionError((AssertionError) e.getCause());
            }
            invokeResult.setException(invokeFailedException);
        } catch (Exception e) {
            log.error("fail to invoke code, cause: {}", Throwables.getStackTraceAsString(e));
            invokeResult.setSuccess(false);
            InvokeFailedException invokeFailedException = new InvokeFailedException();
            invokeFailedException.setMessage(e.getMessage());
            invokeFailedException.setStackTrace(e.getStackTrace());
        }
        return invokeResult;
    }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot 单元测试JUnit的使用详解

    一.简介 JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试. 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人 员是公开的: 回归测试:软件或环境修复或更正后的再测试: 单元测试:最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设计和编码的细节: 二.JUnit使用 1.pom.xml中添加JUnit依赖.

  • Springboot集成JUnit5优雅进行单元测试的示例

    为什么使用JUnit5 JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余. JUnit5易扩展,包容性强,可以接入其他的测试引擎. 功能更强大提供了新的断言机制.参数化测试.重复性测试等新功能. ps:开发人员为什么还要测试,单测写这么规范有必要吗?其实单测是开发人员必备技能,只不过很多开发人员开发任务太重导致调试完就不管了,没有系统化得单元测试,单元测试在系统重构时能发挥巨大的作用,可以在重构后快速测试新的接口是否与重构前有

  • 基于Springboot+Junit+Mockito做单元测试的示例

    前言 这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试. 单元测试前先理解需求 要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测.但本文主要讲mockito的用法,无需关注具体需求.所以本节略去具体的需求描述. 隔离外部依赖 Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值 以被测方法 MatchingServiceImpl.java的matching(Ma

  • SpringBoot环境下junit单元测试速度优化方式

    目录 1.提高单元测试效率 背景 2.单元测试如何执行 补充说明 3.项目中使用 4.优化单测思路 思路 5.实现方式 6.编码实现 6.1 Jetty作为服务启动 6.2 Tomcat作为容器启动 1.提高单元测试效率 背景 在项目提测前,自己需要对代码逻辑进行验证,所以单元测试必不可少. 但是现在的java项目几乎都是基于SpringBoot系列开发的,所以在进行单元测试时,执行一个测试类就要启动springboot项目,加载上下文数据,每次执行一次测试都要再重新加载上下文环境,这样就会很麻

  • 浅谈webpack性能榨汁机(打包速度优化)

    最近对项目的本地开发环境进行了打包速度优化,原有项目,网上能搜到的优化方案基本都加了,在16年低配mac pro 上打包时间为25秒多,但我发现细节做一些调整可能大大降低打包时间,最终优化到7秒多 dll 原有项目是线上和本地公用一套dll配置,因为antd这类ui库需要按需加载所以不能放到dll中,这时可以单独写一个dll配置,将所有第三方库添加到dll中. 这时因为.babelrc中添加了babel-plugin-import插件会导致优化不生效,所以需要对开发环境单独配置babel opt

  • 浅谈Linux环境下gcc优化级别

    代码优化可以说是一个非常复杂而又非常重要的问题,以笔者多年的linux c开发经验来说优化通常分为两个方面,一是人为优化,也就是基于编程经验采用更简易的数据结构函数等来降低编译器负担,二是采用系统自带的优化模式,也就是gcc - o系列,下面我将简述一下各级优化的过程以及实现. gcc - o1 首先o1上面还有一个o0,那个是不提供任何优化,项目中几乎不会使用,而o1使用就非常广泛了,o1是最基本的优化,主要对代码的分支,表达式,常量来进行优化,编译器会在较短的时间下将代码变得更加短小,这样体

  • idea +junit单元测试获取不到bean注入的解决方式

    如图,刚开始报错获取不到bean因为配置文件 1.原因一: *.properties等没有值,还是用${变量的}.获取不到,于是把所有值复制到properties文件里. 2.原因二: springmvc.xml 没有某些静态资源获取报错,把src的resources下的springmvc.xml复制到test目录的resources下,删除静态资源引用. 3.原因三: 可去掉log4j配置. 补充知识:IDEA的junit单元测试Scanner输入无效 在idea的junit单元测试中用Sca

  • SpringBoot 如何通过 Profile 实现不同环境下的配置切换

    目录 一.搭建工程 二.多文件配置方式 三.多片段配置方式 四.使用外部配置文件 SpringBoot 通过 profile 实现在不同环境下的配置切换,比如常见的开发环境.测试环境.生产环境. SpringBoot 常用配置文件主要有 2 种:properties 文件和 yml 文件.对于 properties 文件来说,主要通过多 profile 配置文件的方式来实现:对于 yml 文件来说,主要通过多片段的方式来实现(在一个 yml 文件中通过 3 个横杠来划分配置片段). Profil

  • 在Linux环境下采用压缩包方式安装JDK 13的方法

    什么是JDK? 好吧如果你不知道这个问题的话我实在是不知道你为什么要装这个东西. JDK(Java Development Kit)是Sun公司(后被Oracle收购)推出的面向对象程序设计语言的开发工具包,拥有这个工具包之后我们就可以使用Java语言进行程序设计和开发. 而今天我们要在Linux环境 下对这个东西进行部署以便能够进行开发,并且是以压缩包解压的方式进行安装,之所以不用rpm方式安装主要是为了能够在所有Linux系统上都通用,rpm和deb最多只能在Red Hat和Debian旗下

  • 详解SpringBoot读取resource目录下properties文件的常见方式

    个人理解 在企业开发中,我们经常需要自定义一些全局变量/不可修改变量或者参数来解决大量的变量重复问题,当需要这个全局变量时,只需要从配置文件中读取即可,根据开发中常见的情况,可以分为以下两种情况,分别是: 配置文件为SpringBoot默认的application.properties文件中的自定义参数 加载自定义properties文件中的自定义参数,比如xxx.properties的自定义参数 加载SpringBoot默认的application.properties 准备工作 server

  • centos环境下使用tomcat 部署SpringBoot的war包

    准备war包 一.准备好已有的SpringBoot工程,在pom中添加依赖 1)设置打包格式为war <packaging>war</packaging> 2)排除SpringBoot内嵌的tomcat <!-- 以war包部署的形式需要排除内嵌的tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-bo

  • Java指令重排在多线程环境下的解决方式

    目录 一.序言 二.问题复原 (一)关联变量 1.结果预测 2.指令重排 (二)new创建对象 1.解析创建过程 2.重排序过程分析 三.应对指令重排 (一)AtomicReference原子类 (二)volatile关键字 四.指令重排的理解 1.指令重排广泛存在 2.多线程环境指令重排 3.synchronized锁与重排序无关 一.序言 指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响:在多线程环境下,指令重排会给程序带来意想不到的错误. 本文对多线程指令重排问题进行

  • SpringBoot使用 druid 连接池来优化分页语句

    一.前言 一个老系统随着数据量越来越大,我们察觉到部分分页语句拖慢了我们的速度. 鉴于老系统的使用方式,不打算使用pagehelper和mybatis-plus来处理,加上系统里使用得是druid连接池,考虑直接使用druid来优化. 二.老代码 老代码是使用得一个mybatis插件进行的分页,分页的核心代码如下: // 记录统计的 sql String countSql = "select count(0) from (" + sql+ ") tmp_count"

随机推荐