溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

SpringBoot環(huán)境下junit單元測試速度優(yōu)化方式的示例分析

發(fā)布時間:2021-09-03 13:20:27 來源:億速云 閱讀:220 作者:小新 欄目:開發(fā)技術(shù)

這篇文章主要介紹SpringBoot環(huán)境下junit單元測試速度優(yōu)化方式的示例分析,文中介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們一定要看完!

1、提高單元測試效率

背景

在項目提測前,自己需要對代碼邏輯進(jìn)行驗證,所以單元測試必不可少。

但是現(xiàn)在的java項目幾乎都是基于SpringBoot系列開發(fā)的,所以在進(jìn)行單元測試時,執(zhí)行一個測試類就要啟動springboot項目,加載上下文數(shù)據(jù),每次執(zhí)行一次測試都要再重新加載上下文環(huán)境,這樣就會很麻煩,浪費時間;在一次項目中,我們使用自己的技術(shù)框架進(jìn)行開發(fā),每次單元測試時都要初始化很多數(shù)據(jù)(例如根據(jù)數(shù)據(jù)模型建立表,加載依賴其它模塊的類),這樣導(dǎo)致每一次單元測試時都會花3-5分鐘時間(MacOs 四核Intel Core i5 內(nèi)存:16g),所以很有必要優(yōu)化單元測試效率,節(jié)約開發(fā)時間。

2、單元測試如何執(zhí)行

首先要優(yōu)化單元測試,那要知道單元測試是怎樣執(zhí)行的

引入相關(guān)測試的maven依賴,例如junit,之后在測試方法加上@Test注解即可,在springboot項目測試中還需要在測試類加上@RunWith注解 然后允許需要測試的方法即可

補充說明

  • @RunWith 就是一個運行器

  • @RunWith(JUnit4.class) 就是指用JUnit4來運行

  • @RunWith(SpringJUnit4ClassRunner.class),讓測試運行于Spring測試環(huán)境

  • @RunWith(Suite.class) 的話就是一套測試集合,

  • @ContextConfiguration Spring整合JUnit4測試時,使用注解引入多個配置文件@RunWith

SpringBoot環(huán)境下單元測試一般是加@RunWith(SpringJUnit4ClassRunner.class)注解,SpringJUnit4ClassRunner繼承BlockJUnit4ClassRunner類,然后在測試方式時會執(zhí)行SpringJUnit4ClassRunner類的run方法(重寫了BlockJUnit4ClassRunner的run方法),run方法主要是初始化spring環(huán)境數(shù)據(jù),與執(zhí)行測試方法

3、項目中使用

在我們項目中,是通過一個RewriteSpringJUnit4ClassRunner類繼承SpringJUnit4ClassRunner,然后@RunWith(RewriteSpringJUnit4ClassRunner.class)來初始化我們框架中需要的數(shù)據(jù),

RewriteSpringJUnit4ClassRunner里面是通過重寫withBefores方法,在withBefores方法中去初始化數(shù)據(jù)的,之后通過run方法最后代理執(zhí)行測試方法

4、優(yōu)化單測思路

通過上面說明,可以知道每次測試一個方法都要初始化springboot環(huán)境與加載自己框架的數(shù)據(jù),所以有沒有一種方式可以只需要初始化 一次數(shù)據(jù),就可以反復(fù)運行測試的方法呢?

思路

首先每一次單測都需要重新加載數(shù)據(jù),跑完一次程序就結(jié)束了,所以每次測試方法時都要重新加載數(shù)據(jù),

如果只需要啟動一次把環(huán)境數(shù)據(jù)都加載了,然后之后都單元測試方法都使用這個環(huán)境呢那不就能解決這個問題么。

我們是不是可以搞一個服務(wù)器,把基礎(chǔ)環(huán)境與數(shù)據(jù)都加載進(jìn)去,然后每次執(zhí)行單元測試方法時,通過服務(wù)器代理去執(zhí)行這個方法,不就可以了嗎

5、實現(xiàn)方式

首先我們可以用springboot的方式啟動一個服務(wù),通常使用的內(nèi)置tomcat作為服務(wù)啟,之后暴露一個http接口,入?yún)樾枰獔?zhí)行的類和方法,然后通過反射去執(zhí)行這個方法;還可以通過啟動jetty服務(wù),通過jetty提供的handler處理器就可以處理請求,jetty相對于tomcat處理請求更加方便

服務(wù)是有了,那怎樣將單元測試方法代理給服務(wù)器呢?前面提到過,通過@RunWith注入的類,在單元測試方法運行時會執(zhí)行@RunWith注入的類相應(yīng)的方法,所以我們可以在@RunWith注入的類里面做文章,拿到測試類與方法,然后通過http訪問服務(wù)器,然后服務(wù)器去代理執(zhí)行測試方法

6、編碼實現(xiàn)

下面將通過兩種不同方式實現(xiàn),以Jetty為服務(wù)器啟動,與以Tomcat為服務(wù)器啟動

6.1 Jetty作為服務(wù)啟動

首先編寫服務(wù)啟動類,并在spring容器準(zhǔn)備好后加載我們公司框架相關(guān)數(shù)據(jù),這里使用jetty作為服務(wù)器,下面代碼是核心方法

// 只能寫在測試目錄下,因為寫在應(yīng)用程序目錄下在序列化時,找不到測試目錄下的類-》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) {
        // 加載框架數(shù)據(jù)
    }
    @Bean
    public JettyServer jettyServer(ApplicationContext applicationContext) {
        return new JettyServer(port, applicationContext);
    }
}

使用jetty作為服務(wù)器,并且注入處理器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 {
            // 遠(yuǎn)程代理執(zhí)行
            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;
    }
    /**
     * 遞交給遠(yuǎn)程執(zhí)行
     *
     * @param method   執(zhí)行的方法
     * @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請求訪問服務(wù)器
            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("遠(yuǎn)程執(zhí)行失敗")));
            }
            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;
    }
}

以上是“SpringBoot環(huán)境下junit單元測試速度優(yōu)化方式的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI