SpringBoot環(huán)境下junit單元測試速度優(yōu)化方式
1、提高單元測試效率
背景
在項目提測前,自己需要對代碼邏輯進行驗證,所以單元測試必不可少。
但是現(xiàn)在的java項目幾乎都是基于SpringBoot系列開發(fā)的,所以在進行單元測試時,執(zhí)行一個測試類就要啟動springboot項目,加載上下文數(shù)據,每次執(zhí)行一次測試都要再重新加載上下文環(huán)境,這樣就會很麻煩,浪費時間;在一次項目中,我們使用自己的技術框架進行開發(fā),每次單元測試時都要初始化很多數(shù)據(例如根據數(shù)據模型建立表,加載依賴其它模塊的類),這樣導致每一次單元測試時都會花3-5分鐘時間(MacOs 四核Intel Core i5 內存:16g),所以很有必要優(yōu)化單元測試效率,節(jié)約開發(fā)時間。
2、單元測試如何執(zhí)行
首先要優(yōu)化單元測試,那要知道單元測試是怎樣執(zhí)行的
引入相關測試的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ù)據,與執(zhí)行測試方法
3、項目中使用
在我們項目中,是通過一個RewriteSpringJUnit4ClassRunner類繼承SpringJUnit4ClassRunner,然后@RunWith(RewriteSpringJUnit4ClassRunner.class)來初始化我們框架中需要的數(shù)據,
RewriteSpringJUnit4ClassRunner里面是通過重寫withBefores方法,在withBefores方法中去初始化數(shù)據的,之后通過run方法最后代理執(zhí)行測試方法
4、優(yōu)化單測思路
通過上面說明,可以知道每次測試一個方法都要初始化springboot環(huán)境與加載自己框架的數(shù)據,所以有沒有一種方式可以只需要初始化 一次數(shù)據,就可以反復運行測試的方法呢?
思路
首先每一次單測都需要重新加載數(shù)據,跑完一次程序就結束了,所以每次測試方法時都要重新加載數(shù)據,
如果只需要啟動一次把環(huán)境數(shù)據都加載了,然后之后都單元測試方法都使用這個環(huán)境呢那不就能解決這個問題么。
我們是不是可以搞一個服務器,把基礎環(huán)境與數(shù)據都加載進去,然后每次執(zhí)行單元測試方法時,通過服務器代理去執(zhí)行這個方法,不就可以了嗎
5、實現(xiàn)方式
首先我們可以用springboot的方式啟動一個服務,通常使用的內置tomcat作為服務啟,之后暴露一個http接口,入參為需要執(zhí)行的類和方法,然后通過反射去執(zhí)行這個方法;還可以通過啟動jetty服務,通過jetty提供的handler處理器就可以處理請求,jetty相對于tomcat處理請求更加方便
服務是有了,那怎樣將單元測試方法代理給服務器呢?前面提到過,通過@RunWith注入的類,在單元測試方法運行時會執(zhí)行@RunWith注入的類相應的方法,所以我們可以在@RunWith注入的類里面做文章,拿到測試類與方法,然后通過http訪問服務器,然后服務器去代理執(zhí)行測試方法
6、編碼實現(xiàn)
下面將通過兩種不同方式實現(xiàn),以Jetty為服務器啟動,與以Tomcat為服務器啟動
6.1 Jetty作為服務啟動
首先編寫服務啟動類,并在spring容器準備好后加載我們公司框架相關數(shù)據,這里使用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) { // 加載框架數(shù)據 } @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 { // 遠程代理執(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; } /** * 遞交給遠程執(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請求訪問服務器 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("遠程執(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; } }
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Java Web項目部署在Tomcat運行出錯與解決方法示例
這篇文章主要介紹了Java Web項目部署在Tomcat運行出錯與解決方法,結合具體實例形式分析了Java Web項目部署在Tomcat過程中由于xml配置文件導致的錯誤問題常見提示與解決方法,需要的朋友可以參考下2017-03-03關于@ResponseBody 默認輸出的誤區(qū)的解答
這篇文章主要介紹了關于@ResponseBody 默認輸出的誤區(qū)的解答,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-04-04Spring使用AspectJ的注解式實現(xiàn)AOP面向切面編程
這篇文章主要介紹了Spring使用AspectJ的注解式實現(xiàn)AOP面向切面編程的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06Java中wait與sleep的區(qū)別講解(wait有參及無參區(qū)別)
這篇文章主要介紹了Java中wait與sleep的講解(wait有參及無參區(qū)別),通過代碼介紹了wait()?與wait(?long?timeout?)?區(qū)別,wait(0)?與?sleep(0)區(qū)別,需要的朋友可以參考下2022-04-04