首页 > 编程语言 > SpringBoot环境下junit单元测试速度优化方式
2023
03-16

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

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;
    }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持自学编程网。

编程技巧