单元测试速度优化

问题发现

写完逻辑需要跑一个单元测试,来测试数据的结果,每次启动测试方法都先加载上下文(相当于启动项目)然后才定位到对应的测试方法去执行,其中加载上下文非常耗时,每次运行一个测试方法,就要花费4到5分钟的时间,非常需要优化一下。

单元测试运行原理

  • 引入maven依赖,在对应的测试方法上标注@Test注解
  • @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环境数据,与执行测试方法

优化思路

  • 首先,先启动上下文,一次启动,每次运行测试方法都用启动好的那个上下文的数据
  • 其次,自定义执行器,继承:BlockJUnit4ClassRunner
  • 暴露接口,处理测试方法,post提交,提交测试方法与测试类,使用Spring的bean注入,将测试类作为bean注入到容器中,反射执行测试方法

实现细节

自定义执行器

class SpringDelegateRunner(private val testClass: Class<*>?) : BlockJUnit4ClassRunner(testClass) {

  private val mapper = createObjectMapper()


  override fun runChild(method: FrameworkMethod?, notifier: RunNotifier?) {
    val description = describeChild(method)
    if (isIgnored(method)) {
      notifier!!.fireTestIgnored(description)
      return
    }
    if (method == null || notifier == null) {
      return
    }
    val invokeRequest = InvokeRequest()
    invokeRequest.testClass = method.declaringClass
    invokeRequest.methodName = method.name

    try {
      notifier.fireTestStarted(description)
      val json = mapper.writeValueAsString(invokeRequest)
      val resultBody = HttpRequest.post("http://127.0.0.1:9210/do1/junit/test")
        .body(json)
        .execute().body()
      if (StringUtils.isBlank(resultBody)) {
        notifier.fireTestFailure(Failure(description, RuntimeException("远程执行失败!")))
      }
      val invokeResult = mapper.readValue(resultBody, InvokeResult::class.java)
      val success = invokeResult.success
      if (success) {
        notifier.fireTestFinished(description)
      } else {
        val exception = invokeResult.exception
        if (exception?.assertionError != null) {
          notifier.fireTestFailure(Failure(description, exception.assertionError))
        } else {
          notifier.fireTestFailure(Failure(description, RuntimeException("执行失败!!!!!!")))
        }
      }
    } catch (e: Exception) {
      notifier.fireTestFailure(Failure(description, e))
    }
//    super.runChild(method, notifier)
  }
}

暴露接口,接收执行器的请求

@Controller
@RequestMapping("do1/junit")
class TestController {

  private val mapper = createObjectMapper()
  @Autowired
  private lateinit var applicationContext: ApplicationContext

  private val log: Logger = LoggerFactory.getLogger(this::class.java)

  private val methodMap: ConcurrentHashMap<String, Method> = ConcurrentHashMap<String, Method>()

  @PostMapping("/test")
  fun test(request: HttpServletRequest, response: HttpServletResponse) {
    val contentLength = request.contentLength
    val inputStream: ServletInputStream
    var buffer: ByteArray? = null
    try {
      inputStream = request.inputStream
      buffer = ByteArray(contentLength)
      inputStream.read(buffer, 0, contentLength)
      inputStream.close()
    } catch (e: IOException) {
      e.printStackTrace()
    }
    try {
      val invokeRequest: InvokeRequest = mapper.readValue(buffer, InvokeRequest::class.java)
      val execute: InvokeResult = execute(invokeRequest)
      val result: String = mapper.writeValueAsString(execute)
      log.info("===================$result")
      response.setHeader("Content-Type", "application/json")
      response.writer.write(result)
      response.writer.close()
    } catch (e: Exception) {
      e.printStackTrace()
    }
  }

  private fun registerBeanOfType(type: Class<*>) {
    val beanDefinition: BeanDefinition = GenericBeanDefinition()
    beanDefinition.beanClassName = type.name
    ((applicationContext as GenericApplicationContext).beanFactory as DefaultListableBeanFactory)
      .registerBeanDefinition(type.name, beanDefinition)
  }

  private fun getMethod(clazz: Class<*>, methodName: String): Method? {
    val key = clazz.canonicalName + ":" + methodName
    var md: Method? = null
    if (methodMap.containsKey(key)) {
      md = methodMap[key]
    } else {
      val methods = clazz.methods
      for (mth in methods) {
        if (mth.name == methodName) {
          methodMap.putIfAbsent(key, mth)
          md = mth
          break
        }
      }
    }
    return md
  }

  private fun execute(invokeRequest: InvokeRequest): InvokeResult {
    val testClass: Class<*> = invokeRequest.testClass?: return InvokeResult()
    val bean = try {
      applicationContext.getBean(testClass.name)
    } catch (e: java.lang.Exception) {
      registerBeanOfType(testClass)
      applicationContext.getBean(testClass.name)
    }
    val invokeResult = InvokeResult()
    val method = getMethod(testClass, invokeRequest.methodName!!)
    try {
      val start = System.currentTimeMillis()
      log.info("开始调用测试方法:")
      method!!.invoke(bean)
      log.info("调用测试方法结束, 耗时: ${(System.currentTimeMillis() - start) / 1000} 秒")
      invokeResult.success = true
    } catch (e: IllegalAccessException) {
      if (e !is InvocationTargetException
        || (e as InvocationTargetException).targetException !is AssertionError
      ) {
        log.error("fail to invoke code, cause: {}", e)
      }
      invokeResult.success = false
      val invokeFailedException = InvokeFailedException()
      invokeFailedException.message = e.message.toString()
      invokeFailedException.stackTrace = e.stackTrace

      // 由Assert抛出来的错误
      if (e.cause is AssertionError) {
        invokeFailedException.assertionError = e.cause as AssertionError?
      }
      invokeResult.exception = invokeFailedException
    } catch (e: IllegalArgumentException) {
      if (e !is InvocationTargetException
        || (e as InvocationTargetException).targetException !is AssertionError
      ) {
        log.error("fail to invoke code, cause: {}", e)
      }
      invokeResult.success = false
      val invokeFailedException = InvokeFailedException()
      invokeFailedException.message = e.message.toString()
      invokeFailedException.stackTrace = e.stackTrace
      if (e.cause is AssertionError) {
        invokeFailedException.assertionError = e.cause as AssertionError?
      }
      invokeResult.exception = invokeFailedException
    } catch (e: InvocationTargetException) {
      if (e !is InvocationTargetException
        || e.targetException !is AssertionError
      ) {
        log.error("fail to invoke code, cause: {}", e)
      }
      invokeResult.success = false
      val invokeFailedException = InvokeFailedException()
      invokeFailedException.message = e.message.toString()
      invokeFailedException.stackTrace = e.stackTrace
      if (e.cause is AssertionError) {
        invokeFailedException.assertionError = e.cause as AssertionError?
      }
      invokeResult.exception = invokeFailedException
    } catch (e: java.lang.Exception) {
      log.error("fail to invoke code, cause: {}", e)
      invokeResult.success = false
      val invokeFailedException = InvokeFailedException()
      invokeFailedException.message = e.message.toString()
      invokeFailedException.stackTrace = e.stackTrace
    }
    return invokeResult
  }

}

思路来源自:https://blog.csdn.net/qq_37803406/article/details/114778041

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值