這篇文章主要介紹SpringBoot環境下junit單元測試速度優化方式的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
在項目提測前,自己需要對代碼邏輯進行驗證,所以單元測試必不可少。
但是現在的java項目幾乎都是基于SpringBoot系列開發的,所以在進行單元測試時,執行一個測試類就要啟動springboot項目,加載上下文數據,每次執行一次測試都要再重新加載上下文環境,這樣就會很麻煩,浪費時間;在一次項目中,我們使用自己的技術框架進行開發,每次單元測試時都要初始化很多數據(例如根據數據模型建立表,加載依賴其它模塊的類),這樣導致每一次單元測試時都會花3-5分鐘時間(MacOs 四核Intel Core i5 內存:16g),所以很有必要優化單元測試效率,節約開發時間。
首先要優化單元測試,那要知道單元測試是怎樣執行的
引入相關測試的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環境數據,與執行測試方法
在我們項目中,是通過一個RewriteSpringJUnit4ClassRunner類繼承SpringJUnit4ClassRunner,然后@RunWith(RewriteSpringJUnit4ClassRunner.class)來初始化我們框架中需要的數據,
RewriteSpringJUnit4ClassRunner里面是通過重寫withBefores方法,在withBefores方法中去初始化數據的,之后通過run方法最后代理執行測試方法
通過上面說明,可以知道每次測試一個方法都要初始化springboot環境與加載自己框架的數據,所以有沒有一種方式可以只需要初始化 一次數據,就可以反復運行測試的方法呢?
首先每一次單測都需要重新加載數據,跑完一次程序就結束了,所以每次測試方法時都要重新加載數據,
如果只需要啟動一次把環境數據都加載了,然后之后都單元測試方法都使用這個環境呢那不就能解決這個問題么。
我們是不是可以搞一個服務器,把基礎環境與數據都加載進去,然后每次執行單元測試方法時,通過服務器代理去執行這個方法,不就可以了嗎
首先我們可以用springboot的方式啟動一個服務,通常使用的內置tomcat作為服務啟,之后暴露一個http接口,入參為需要執行的類和方法,然后通過反射去執行這個方法;還可以通過啟動jetty服務,通過jetty提供的handler處理器就可以處理請求,jetty相對于tomcat處理請求更加方便
服務是有了,那怎樣將單元測試方法代理給服務器呢?前面提到過,通過@RunWith注入的類,在單元測試方法運行時會執行@RunWith注入的類相應的方法,所以我們可以在@RunWith注入的類里面做文章,拿到測試類與方法,然后通過http訪問服務器,然后服務器去代理執行測試方法
下面將通過兩種不同方式實現,以Jetty為服務器啟動,與以Tomcat為服務器啟動
首先編寫服務啟動類,并在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));
}
}
}@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環境下junit單元測試速度優化方式的示例分析”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。