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