SpringBoot環(huán)境下junit單元測(cè)試速度優(yōu)化方式
1、提高單元測(cè)試效率
背景
在項(xiàng)目提測(cè)前,自己需要對(duì)代碼邏輯進(jìn)行驗(yàn)證,所以單元測(cè)試必不可少。
但是現(xiàn)在的java項(xiàng)目幾乎都是基于SpringBoot系列開(kāi)發(fā)的,所以在進(jìn)行單元測(cè)試時(shí),執(zhí)行一個(gè)測(cè)試類(lèi)就要啟動(dòng)springboot項(xiàng)目,加載上下文數(shù)據(jù),每次執(zhí)行一次測(cè)試都要再重新加載上下文環(huán)境,這樣就會(huì)很麻煩,浪費(fèi)時(shí)間;在一次項(xiàng)目中,我們使用自己的技術(shù)框架進(jìn)行開(kāi)發(fā),每次單元測(cè)試時(shí)都要初始化很多數(shù)據(jù)(例如根據(jù)數(shù)據(jù)模型建立表,加載依賴其它模塊的類(lèi)),這樣導(dǎo)致每一次單元測(cè)試時(shí)都會(huì)花3-5分鐘時(shí)間(MacOs 四核Intel Core i5 內(nèi)存:16g),所以很有必要優(yōu)化單元測(cè)試效率,節(jié)約開(kāi)發(fā)時(shí)間。
2、單元測(cè)試如何執(zhí)行
首先要優(yōu)化單元測(cè)試,那要知道單元測(cè)試是怎樣執(zhí)行的
引入相關(guān)測(cè)試的maven依賴,例如junit,之后在測(cè)試方法加上@Test注解即可,在springboot項(xiàng)目測(cè)試中還需要在測(cè)試類(lèi)加上@RunWith注解 然后允許需要測(cè)試的方法即可
補(bǔ)充說(shuō)明
- @RunWith 就是一個(gè)運(yùn)行器
- @RunWith(JUnit4.class) 就是指用JUnit4來(lái)運(yùn)行
- @RunWith(SpringJUnit4ClassRunner.class),讓測(cè)試運(yùn)行于Spring測(cè)試環(huán)境
- @RunWith(Suite.class) 的話就是一套測(cè)試集合,
- @ContextConfiguration Spring整合JUnit4測(cè)試時(shí),使用注解引入多個(gè)配置文件@RunWith
SpringBoot環(huán)境下單元測(cè)試一般是加@RunWith(SpringJUnit4ClassRunner.class)注解,SpringJUnit4ClassRunner繼承BlockJUnit4ClassRunner類(lèi),然后在測(cè)試方式時(shí)會(huì)執(zhí)行SpringJUnit4ClassRunner類(lèi)的run方法(重寫(xiě)了BlockJUnit4ClassRunner的run方法),run方法主要是初始化spring環(huán)境數(shù)據(jù),與執(zhí)行測(cè)試方法
3、項(xiàng)目中使用
在我們項(xiàng)目中,是通過(guò)一個(gè)RewriteSpringJUnit4ClassRunner類(lèi)繼承SpringJUnit4ClassRunner,然后@RunWith(RewriteSpringJUnit4ClassRunner.class)來(lái)初始化我們框架中需要的數(shù)據(jù),
RewriteSpringJUnit4ClassRunner里面是通過(guò)重寫(xiě)withBefores方法,在withBefores方法中去初始化數(shù)據(jù)的,之后通過(guò)run方法最后代理執(zhí)行測(cè)試方法
4、優(yōu)化單測(cè)思路
通過(guò)上面說(shuō)明,可以知道每次測(cè)試一個(gè)方法都要初始化springboot環(huán)境與加載自己框架的數(shù)據(jù),所以有沒(méi)有一種方式可以只需要初始化 一次數(shù)據(jù),就可以反復(fù)運(yùn)行測(cè)試的方法呢?
思路
首先每一次單測(cè)都需要重新加載數(shù)據(jù),跑完一次程序就結(jié)束了,所以每次測(cè)試方法時(shí)都要重新加載數(shù)據(jù),
如果只需要啟動(dòng)一次把環(huán)境數(shù)據(jù)都加載了,然后之后都單元測(cè)試方法都使用這個(gè)環(huán)境呢那不就能解決這個(gè)問(wèn)題么。
我們是不是可以搞一個(gè)服務(wù)器,把基礎(chǔ)環(huán)境與數(shù)據(jù)都加載進(jìn)去,然后每次執(zhí)行單元測(cè)試方法時(shí),通過(guò)服務(wù)器代理去執(zhí)行這個(gè)方法,不就可以了嗎
5、實(shí)現(xiàn)方式
首先我們可以用springboot的方式啟動(dòng)一個(gè)服務(wù),通常使用的內(nèi)置tomcat作為服務(wù)啟,之后暴露一個(gè)http接口,入?yún)樾枰獔?zhí)行的類(lèi)和方法,然后通過(guò)反射去執(zhí)行這個(gè)方法;還可以通過(guò)啟動(dòng)jetty服務(wù),通過(guò)jetty提供的handler處理器就可以處理請(qǐng)求,jetty相對(duì)于tomcat處理請(qǐng)求更加方便
服務(wù)是有了,那怎樣將單元測(cè)試方法代理給服務(wù)器呢?前面提到過(guò),通過(guò)@RunWith注入的類(lèi),在單元測(cè)試方法運(yùn)行時(shí)會(huì)執(zhí)行@RunWith注入的類(lèi)相應(yīng)的方法,所以我們可以在@RunWith注入的類(lèi)里面做文章,拿到測(cè)試類(lèi)與方法,然后通過(guò)http訪問(wèn)服務(wù)器,然后服務(wù)器去代理執(zhí)行測(cè)試方法
6、編碼實(shí)現(xiàn)
下面將通過(guò)兩種不同方式實(shí)現(xiàn),以Jetty為服務(wù)器啟動(dòng),與以Tomcat為服務(wù)器啟動(dòng)
6.1 Jetty作為服務(wù)啟動(dòng)
首先編寫(xiě)服務(wù)啟動(dòng)類(lèi),并在spring容器準(zhǔn)備好后加載我們公司框架相關(guān)數(shù)據(jù),這里使用jetty作為服務(wù)器,下面代碼是核心方法
// 只能寫(xiě)在測(cè)試目錄下,因?yàn)閷?xiě)在應(yīng)用程序目錄下在序列化時(shí),找不到測(cè)試目錄下的類(lèi)-》InvokeRequest類(lèi)中的Class<?> testClass反序列化不出來(lái)
@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);
// 注冊(cè)處理的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請(qǐng)求
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);
// 記錄異常類(lèi)
InvokeFailedException invokeFailedException = new InvokeFailedException();
invokeFailedException.setMessage(e.getMessage());
invokeFailedException.setStackTrace(e.getStackTrace());
// 由Assert拋出來(lái)的錯(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;
/**
* 測(cè)試方法所在的類(lèi)
*/
private Class<?> testClass;
/**
* 測(cè)試的方法名
*/
private String methodName;
}
編寫(xiě)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請(qǐng)求訪問(wèn)服務(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拋出來(lái)的錯(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)文章
Springboot 整合通用mapper和pagehelper展示分頁(yè)數(shù)據(jù)的問(wèn)題(附github源碼)
這篇文章主要介紹了Springboot 整合通用mapper和pagehelper展示分頁(yè)數(shù)據(jù)(附github源碼),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09
springboot啟動(dòng)時(shí)如何指定spring.profiles.active
這篇文章主要介紹了springboot啟動(dòng)時(shí)如何指定spring.profiles.active問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
MyBatis 探秘之#{} 與 ${} 參傳差異解碼(數(shù)據(jù)庫(kù)連接池筑牢數(shù)據(jù)交互
本文詳細(xì)介紹了MyBatis中的`#{}`和`${}`的區(qū)別與使用場(chǎng)景,包括預(yù)編譯SQL和即時(shí)SQL的區(qū)別、安全性問(wèn)題,以及如何正確使用數(shù)據(jù)庫(kù)連接池來(lái)提高性能,感興趣的朋友一起看看吧2024-12-12
Spring實(shí)現(xiàn)定時(shí)任務(wù)的兩種方法詳解
Spring提供了兩種方式實(shí)現(xiàn)定時(shí)任務(wù),一種是注解,還有一種就是接口了,這篇文章主要為大家介紹了這兩種方法的具體實(shí)現(xiàn)方法,需要的可以參考下2024-12-12
springboot整合curator實(shí)現(xiàn)分布式鎖過(guò)程
這篇文章主要介紹了springboot整合curator實(shí)現(xiàn)分布式鎖過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
Java多線程編程基石ThreadPoolExecutor示例詳解
這篇文章主要為大家介紹了Java多線程編程基石ThreadPoolExecutor示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Java如何實(shí)現(xiàn)通過(guò)鍵盤(pán)輸入一個(gè)數(shù)組
這篇文章主要介紹了Java實(shí)現(xiàn)通過(guò)鍵盤(pán)輸入一個(gè)數(shù)組,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

