You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Spring @WebMvcTest测试方法耗时差异大原因咨询

问题背景

在Kotlin技术栈的Spring项目中使用@WebMvcTest编写集成测试时,不同测试方法执行速度差异较大:test1耗时仅31ms,test2稳定耗时达到800ms,暂未定位到明确诱因。

测试代码实现
@Test
fun `test1`() {
    // given
    val folderList = mutableListOf(
        Folder.FolderListResponse(1L, "emoji1", "child1"),
        Folder.FolderListResponse(2L, "emoji2", "child2")
    )
    every { folderService.findFolderChildList(any()) } returns folderList

    // when
    val resultAction = util.getResultAction("/api/v1/folder/1/children", mockMvc)

    // then
    resultAction
        .andExpect(status().isOk)
        .andExpect(jsonPath("$[0].folderId").value(1L))
        .andExpect(jsonPath("$[0].emoji").value("emoji1"))
        .andExpect(jsonPath("$[0].name").value("child1"))
        .andExpect(jsonPath("$[1].folderId").value(2L))
        .andExpect(jsonPath("$[1].emoji").value("emoji2"))
        .andExpect(jsonPath("$[1].name").value("child2"))
}


@Test
fun `test2`() {
    // given
    val info1 = AccountDto.FolderBelongAccountInfo(1, "info1", "images")
    val info2 = AccountDto.FolderBelongAccountInfo(2, "info2", "images")
    val expected = AccountDto.FolderBelongAccountListDto(mutableListOf(info1, info2))
    every { folderService.getAccountListAtRootFolder(any()) } returns expected

    // when
    val resultAction = util.getResultAction("/api/v1/folder/belong/1", mockMvc)

    // then
    resultAction
        .andExpect(status().isOk)
}


fun getResultAction(uri: String, mockMvc: MockMvc): ResultActions {
    return mockMvc.perform(
        MockMvcRequestBuilders.get(uri)
            .header("AccessToken", "token")
            .header("RefreshToken", "retoken")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .characterEncoding(StandardCharsets.UTF_8.toString())
    )
}
测试执行日志

test1执行日志

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /api/v1/folder
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", AccessToken:"token", RefreshToken:"retoken", Accept:"application/json"]
             Body = null
    Session Attrs = {}

Handler:
             Type = com.yapp.web2.domain.folder.controller.FolderController
           Method = com.yapp.web2.domain.folder.controller.FolderController#findAll(HttpServletRequest)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

test2执行日志

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /api/v1/folder/belong/1
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", AccessToken:"token", RefreshToken:"retoken", Accept:"application/json"]
             Body = null
    Session Attrs = {}

Handler:
             Type = com.yapp.web2.domain.folder.controller.FolderController
           Method = com.yapp.web2.domain.folder.controller.FolderController#getAccountList(long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"list":[{"id":1,"name":"info1","profileImage":"images"},{"id":2,"name":"info2","profileImage":"images"}]}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
性能差异原因

核心差异来自Jackson序列化器的冷启动类加载与初始化开销,和测试业务逻辑本身无关:

  • 从执行日志能看出,当前运行的test1根本没走到预期的子文件夹查询逻辑:代码里写的请求路径是/api/v1/folder/1/children,但实际日志里的请求URI是/api/v1/folder,命中的是findAll接口,返回响应体是空对象{}。序列化空对象不需要加载复杂自定义DTO的序列化逻辑,几乎无额外开销,所以耗时极短。
  • test2是测试类中第一个触发Jackson序列化自定义嵌套DTO的用例:@WebMvcTest上下文中的Jackson序列化模块是懒加载的,第一次遇到非简单类型的自定义DTO(也就是这里的FolderBelongAccountListDto及内部嵌套的FolderBelongAccountInfo类)时,需要完成类反射扫描、对应字段序列化器实例化、类型序列化缓存构建等一系列初始化操作,这部分冷启动开销通常在500-1000ms级别,会计入第一个触发该操作的测试用例耗时里。
  • 这个现象可以很容易复现验证:调整测试执行顺序,或者在test2之前加一个返回任意自定义DTO响应的测试,就会发现耗时高的用例会变成第一个触发复杂DTO序列化的测试,后续所有涉及同类型序列化的测试,耗时都会降到几十毫秒的正常水平。

内容的提问来源于stack exchange,提问作者Leelee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 03:48:10