REST API接口测试实战:MockMvc + JUnit 5从入门到踩坑

写后端测试这几年,我越来越觉得REST API接口测试是“最容易上手、也最容易写废”的环节。MockMvc在Spring MVC项目里几乎是标配的测试工具,JUnit 5解决了很多JUnit 4时代的痛点,两者配合起来,既能单测Controller逻辑,又能验证路由、参数绑定、JSON序列化这些真实请求链路里的关键环节。如果你正在写接口测试但只停留在“跑通不报错”的阶段,或者被MockMvc的API搞得一头雾水,这篇内容应该能帮你把思路理清楚。

这篇文章是JUnit实战系列的第十八章,重点放在“下”半场:REST API接口测试的MockMvc + JUnit 5完整实战。我会从测试环境搭建、GET/POST/PUT/DELETE各种请求的写法,到参数校验、全局异常处理、Mockito隔离外部依赖,再到我实际踩过的坑,逐层拆开讲。适合已经会写基础JUnit 5用例、但想把Controller层测试做得更系统和深入的开发者。

1. 测试环境准备:最小依赖还是完整起步依赖?

1.1 依赖导入与版本兼容

先说依赖。Spring Boot项目里做MockMvc测试,最省事的做法是引入spring-boot-starter-test,这个starter会自动带进来JUnit 5、Mockito、AssertJ、JsonPath等一系列测试库,不用一个个手动加版本号。从Spring Boot 2.2开始,默认的JUnit平台就是JUnit 5,所以如果你的项目还在用JUnit 4跑测试,大概率是pom里显式排掉了junit-jupiter,或者用的是老版本Spring Boot。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

这里有一个非常重要的细节:spring-boot-starter-test内部是帮你管理了依赖版本的,但你如果要手动引入junit-jupiter相关依赖,一定要保证版本号和Spring Boot对应的版本匹配,否则会出现NoSuchMethodError这类奇怪的问题。我见过一个项目,Spring Boot是2.7,但JUnit手动写了5.7.0,结果跑测试直接报junit.platform.launcher相关的类找不到。这种问题排查起来很费时间,最简单的办法就是只用starter自动管理的版本,别手动覆盖。

再补充一点,如果你的项目是Spring Boot 3.x,那底层已经是Jakarta EE 9+了,包名从javax.servlet变成了jakarta.servlet,MockMvc的使用方式本身没变,但如果你在IDE里看到导入javax.servlet.http.HttpServletRequest报错,说明项目已经升级到Jakarta,别把旧包名混进去。

1.2 测试基类与MockMvc实例化方式

MockMvc有两种常见的装配方式,一种是把整个Spring上下文拉起来,一种是只切Web层。我给你看两种的完整写法。

第一种:完整上下文测试(适合做集成测试,连数据库、消息队列一起验证)

@SpringBootTest @AutoConfigureMockMvc class BookApiIntegrationTest { @Autowired private MockMvc mockMvc; @Test void contextLoads() { // 如果能跑到这里,说明上下文启动没有问题 } }

第二种:只切Web层,Service用Mockito打桩(适合做纯Controller单元测试,速度极快)

@WebMvcTest(BookController.class) class BookControllerSliceTest { @Autowired private MockMvc mockMvc; @MockBean private BookService bookService; @Test void shouldReturnBookWhenServiceReturnsData() { // ... } }

很多人刚开始会纠结选哪种。我的建议很简单:如果你要测的接口不依赖外部系统,或者你可以用Testcontainers把依赖都准备好,那就用@SpringBootTest,它能验证到真实Bean装配、事务、安全配置这些;如果你只想快速验证Controller层的逻辑分支,比如参数解析、状态码、响应结构,那用@WebMvcTest省下的启动时间非常可观。

我实测的项目里,一个中等规模的后端服务(几十个Controller),@SpringBootTest启动上下文大概要8到12秒,@WebMvcTest只需要1到2秒。如果在CI上跑全量测试,这个时间差距就很可观了。

另外一个容易被忽略的点是:如果你用了@WebMvcTest,默认不会扫描@Component、@Service、@Repository这些Bean,所以你必须在测试类里把需要的依赖全部通过@MockBean或@Import补齐。Controller里如果注入了RestTemplate、WebClient这类客户端,也是需要手动mock的。

1.3 测试基类的抽取设计

我在多个项目里都建议建一个测试基类,把公共的MockMvc实例、ObjectMapper实例、通用的工具方法放在里面,每个模块的测试类去继承。这样做的好处是避免每个测试类都重复注入。

@SpringBootTest @AutoConfigureMockMvc public abstract class BaseApiTest { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; protected String toJson(Object obj) throws JsonProcessingException { return objectMapper.writeValueAsString(obj); } }

子类测试就变得很干净:

class BookApiTest extends BaseApiTest { @Test void createBook_shouldReturnCreated() throws Exception { String json = toJson(new CreateBookRequest("Spring实战", "Marchi")); mockMvc.perform(post("/api/books") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isCreated()); } }

基类里我特别推荐放一个objectMapper,因为写请求体时会频繁用到,每次手动new都会造成Jackson配置不一致,比如LocalDateTime的序列化格式可能和项目里不同,导致测试请求体和生产环境的格式对不上。最后定位半天才发现是ObjectMapper配置不一致,这种坑我踩过很多次。

2. 第一个MockMvc测试:GET请求的三种常见玩法

2.1 状态码、响应头、JSON体三层验证

MockMvc和真实HTTP请求的差异在于:它不经过真实的Socket端口,而是在内部模拟请求分发到DispatcherServlet执行。所以你不能用它来验证网络层的问题,但它足够验证从路由匹配到Controller返回值这一整条链路。

最基础的GET请求测试长这样:

mockMvc.perform(get("/api/books/1")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.title").value("深入理解REST架构")) .andExpect(jsonPath("$.author").value("Marchi"));

我把验证拆成三层来讲:

第一层,status().isOk()验证HTTP状态码。这里要注意,isOk()对应200,isCreated()对应201,isNoContent()对应204。如果你的接口返回202 Accepted,那就得用status().isAccepted(),别用isOk()糊弄过去,否则测试对状态码的约束就失效了。

第二层,content().contentType()验证响应头里的Content-Type。这个经常被忽略,但很有用。比如一个接口应该返回application/json,结果因为配置问题返回了text/plain,客户端会解析失败。加上这一层断言能提前发现响应头配置错误。

第三层,jsonPath()验证JSON主体。这是MockMvc最强大的地方,它通过JsonPath表达式从响应体里提取数据,然后和你期望的值比对。

2.2 jsonPath的常用表达式与动态字段处理

JsonPath表达式刚开始接触会有点懵,其实就是类似XPath之于XML。我常用的有这些:

// 取普通字段 jsonPath("$.title").value("某本书") // 取嵌套字段 jsonPath("$.author.name").value("Marchi") // 取数组长度 jsonPath("$.tags.length()").value(3) // 取数组中的元素 jsonPath("$.tags[0]").value("java") // 判断字段是否存在 jsonPath("$.id").exists() // 判断字段类型 jsonPath("$.price").isNumber() jsonPath("$.title").isString()

关于动态字段的验证,比如创建接口返回的ID是数据库自增的,你再怎么精确预测,每次值都可能变。这时候就不要用value()去断言具体ID,用exists()只验证它存在,然后再单独查询一次确认数据真的落库了,这样测试既有约束力又不会因为ID变化而频繁失败。

还有一类场景是分页接口,返回结构是{ "content": [], "totalPages": 2 }。你可以这样断言:

jsonPath("$.content").isArray() jsonPath("$.content.length()").value(10) jsonPath("$.totalPages").value(2) jsonPath("$.totalElements").value(20)

2.3 请求参数与路径变量的传参方式

GET接口通常有两种传参方式,MockMvc对应也有两种写法。

路径变量风格:

mockMvc.perform(get("/api/books/{id}", 1L)) .andExpect(status().isOk());

这种方式要注意,{id}是URI模板占位符,后面的参数会自动进行字符串转换。如果传入的值带特殊字符,比如/api/books/abc/def,那路径就乱了。所以占位符的值最好预先做URL编码。

查询参数风格:

mockMvc.perform(get("/api/books") .param("page", "0") .param("size", "10") .param("sort", "title,asc")) .andExpect(status().isOk());

.param()方法很贴心,它会自动处理URL编码,你不用关心?和&的拼接。多个同名的参数也可以重复调用.param("tag", "java").param("tag", "spring"),模拟出来的效果和使用方在浏览器地址栏输入?tag=java&tag=spring是一样的。这一点对测试多值筛选接口特别有用。

还有一个细节:参数里如果传的是数字,MockMvc默认会转成String再绑定到Controller的@RequestParam Integer page,一般情况下没问题。但如果你加了@Validated并且对参数做了@Min、@Max校验,要刻意测一下越界值,比如page=-1应该返回400,这样是对参数约束的一种回归保护。

3. POST/PUT/DELETE:带请求体和鉴权的接口怎么测

3.1 ObjectMapper序列化与@RequestBody验证

POST接口和GET最大的区别在于请求体。我习惯把所有请求参数封装成独立的DTO,测试时用ObjectMapper把DTO序列化成JSON字符串,再放到请求体里。

@Test void createBook_shouldReturn201() throws Exception { CreateBookRequest req = new CreateBookRequest("Spring实战", "Marchi", 59.0); mockMvc.perform(post("/api/books") .contentType(MediaType.APPLICATION_JSON) .content(toJson(req))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").exists()) .andExpect(jsonPath("$.title").value("Spring实战")); }

这里有一个很关键的陷阱:contentType必须设置为APPLICATION_JSON,否则Spring MVC的@RequestBody参数解析可能不会触发,接口直接返回415 Unsupported Media Type。这个错误我见过太多人犯了,一看到415就懵,其实原因就是contentType没设置或者写成了MediaType.TEXT_PLAIN。

另一个陷阱是ObjectMapper对日期和时间的序列化。如果项目里的LocalDateTime字段使用了@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")这种注解,你在测试里new一个ObjectMapper是感知不到这些注解的,序列化出来的格式可能完全不对。所以我才在前面强调,测试基类里一定要注入Spring容器里的ObjectMapper,它已经配置了项目里所有自定义的序列化器和注解。

3.2 请求头与鉴权Token的注入

很多接口有登录鉴权,测试时需要带上token。这时候用.header()方法直接设置请求头,简单粗暴:

mockMvc.perform(get("/api/books/mine") .header(HttpHeaders.AUTHORIZATION, "Bearer test-token-12345")) .andExpect(status().isOk());

在实际项目中,我更推荐把这些公共请求头封装成工具方法,避免每个测试都写一大串重复代码。比如在基类里加一个方法:

protected RequestBuilder authorizedGet(String url, String token) { return get(url).header(HttpHeaders.AUTHORIZATION, "Bearer " + token); }

但这里需要提醒一下:如果你启用了Spring Security,并且有自定义的OncePerRequestFilter解析token,那MockMvc测试跑的是完整的安全链路,token必须是真实可被解析的格式。有些项目会把token解析逻辑包装成返回用户信息的过滤器,这种情况下光是mock一个token字符串是不够的,如果SecurityContext没有认证,接口会返回401。

两种解决思路:第一种是测试时放行安全配置,通过@WithMockUser来指定当前用户;第二种是在测试里构造一个真正的token。@WithMockUser用起来最方便:

@Test @WithMockUser(username = "marchi", roles = "ADMIN") void deleteBook_shouldReturn204() throws Exception { mockMvc.perform(delete("/api/books/1")) .andExpect(status().isNoContent()); }

但要注意,@WithMockUser设置的是Spring Security上下文里的认证信息,它并不会自动把用户信息注入到你的Controller参数里。如果你的Controller通过@AuthenticationPrincipal UserDetails user这种方式获取当前用户,那@WithMockUser的默认值是可以匹配的。

3.3 嵌套JSON与集合类型的响应断言

REST API返回嵌套JSON很常见,比如一个Book对象里有作者信息,作者里又有出版社信息。用JsonPath逐层取就非常方便:

jsonPath("$.author.publisher.name").value("某出版社")

当然也可以取整个嵌套对象返回Map,再通过equalTo进行比较。不过我更推荐JsonPath分字段断言,因为一旦整个嵌套对象序列化顺序或多余字段有变化,equalTo比对特别容易碎,分字段断言更稳定。

集合类型和响应包装也是非常常见的场景。很多公司的接口规范都会定义统一的响应包装类,比如Result<T>、ApiResponse<T>之类。测试时不要只验证$.code和$.message,更要深入到$.data里的具体内容:

mockMvc.perform(get("/api/books?size=2")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.content[0].title").value("Spring实战")) .andExpect(jsonPath("$.data.content[1].title").value("深入理解REST架构"));

这里我要强调一个容易误判的点:Controller返回的如果是ResponseEntity<Result<List<Book>>>,MockMvc的JsonPath一样可以逐层穿透。你不需要在测试里去反序列化整个响应体,JsonPath天然就是为了做这类断言设计的。但如果你需要做更复杂的断言,比如比较整个列表的内容,ObjectMapper.readValue(...)抓取响应体转对象也是个办法。

4. 参数校验与全局异常处理:错误路径也要覆盖

4.1 @Valid触发校验失败时的响应断言

接口测试只测正常路径是不够的,异常和边界路径恰恰是容易出Bug的地方。Spring MVC里参数校验最常见的写法是Controller方法参数上加@Valid,DTO字段上加各种校验注解。

@PostMapping("/api/books") public ResponseEntity<Book> createBook(@Valid @RequestBody CreateBookRequest req) { // ... }

测试时故意传一个非法请求体:

CreateBookRequest invalidReq = new CreateBookRequest("", "Marchi", -10.0); mockMvc.perform(post("/api/books") .contentType(MediaType.APPLICATION_JSON) .content(toJson(invalidReq))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").exists());

这里要注意,@Valid校验失败后Spring默认返回400 Bad Request,错误响应体结构是的DefaultHandlerExceptionResolver或ResponseEntityExceptionHandler决定的。如果你的项目里有全局异常处理器@RestControllerAdvice,那错误响应的格式可能完全不一样。要确认你使用的框架版本下校验失败时具体返回哪些字段,不要想当然以为$.message一定存在——如果全局异常处理器把错误信息放在$.error或$.errors[0].message里,你的断言就会失败。

4.2 自定义异常和ControllerAdvice的测试衔接

很多项目的错误处理会统一交到@RestControllerAdvice,比如抛出BusinessException时返回特定的错误码和HTTP状态码。测试这类逻辑时,我不建议真的在测试里制造业务异常,而是通过Mockito让Service层抛出异常,再验证Controller的响应:

@Test void updateBook_whenBookNotFound_shouldReturn404() throws Exception { when(bookService.updateBook(eq(100L), any(UpdateBookRequest.class))) .thenThrow(new BookNotFoundException("book not found")); mockMvc.perform(put("/api/books/100") .contentType(MediaType.APPLICATION_JSON) .content(toJson(updateReq))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value("BOOK_NOT_FOUND")); }

这个测试验证的是从Controller抛出的异常到@RestControllerAdvice兜底处理这整条链路是否正常工作。如果全局异常处理器里忘记了对BookNotFoundException的映射,这个测试会直接暴露出来,非常实用。

4.3 404、405、500等状态码模拟

MockMvc不仅可以走Controller逻辑,还能模拟路由匹配不到的情况。当你请求一个不存在路径时:

mockMvc.perform(get("/api/nonexistent")) .andExpect(status().isNotFound());

如果路径拼错了但对上了别名,或者请求方法不对,就可能返回405。比如接口只支持POST,你用GET去请求:

mockMvc.perform(get("/api/books")) .andExpect(status().isMethodNotAllowed()); // 405

这种测试能发现一个实际问题:是否有人在设计接口时一个URL对应了多个不同的HTTP方法,而框架配置没有跟上。

还有一个有意思的场景是500状态码的模拟。如果你想让接口真的抛一个未捕获的RuntimeException,最好克隆一个专门用来测试异常的Service实现,或者用Mockito的doThrow。注意不要为了制造500而在生产代码里埋雷,测试完就有意保留。

我自己写错误路径测试的时候有一个原则:对4xx的断言尽量具体,对5xx的断言保持结构层面即可,不要断言过于具体的错误文案,因为异常文案很容易改动,但状态码和错误码通常是稳定的接口契约。

5. MockMvc + Mockito:把外部服务隔离掉的测试方案

5.1 对Service层打桩,controller测业务编排

当Controller已经相对成熟,重点关注的是接口层的逻辑时,用Mockito打桩Service层,配合@WebMvcTest做Controller切片测试,是我最常用的一套组合。

假设BookController依赖BookService:

@WebMvcTest(BookController.class) class BookControllerSliceTest { @Autowired private MockMvc mockMvc; @MockBean private BookService bookService; @Test void getBook_shouldReturnBook() throws Exception { Book book = new Book(1L, "Spring实战", "Marchi", 59.0); when(bookService.findById(1L)).thenReturn(book); mockMvc.perform(get("/api/books/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.title").value("Spring实战")); } }

这里有个必须要提醒的点:@MockBean是Spring Boot测试提供的注解,它会向ApplicationContext里注册一个mock版本的Bean,替代原有的真实Bean。所以在切片测试里,你打桩的是Service接口,Controller里注入的就是这个mock实例。如果你没有对某个依赖方法打桩,Mockito默认返回空值(null、空集合等),Controller里如果对这些空值做了不必要的处理,测试会马上暴露出来。

5.2 verify验证调用次数,防止无效刷新

除了打桩返回值,Mockito的verify经常用来验证交互行为。比如一个刷新接口,如果某个参数为空,刷新的方法就不应该被调用:

@Test void refreshCache_whenParamMissing_shouldNotCallService() throws Exception { mockMvc.perform(post("/api/cache/refresh") .param("force", "false")) .andExpect(status().isAccepted()); verify(cacheService, never()).forceRefresh(); }

verify还能用来确认调用参数是否符合预期:

verify(bookService, times(1)).findById(1L);

有时候我在测试结束前加一个verifyNoMoreInteractions来强制校验没有其他多余调用,这可以避免Controller悄悄多调了一个Service方法而没人发现。不过这个用法要克制,否则接口一增加新逻辑测试就碎,维护成本会直线上升。

5.3 避免过度mock的实践建议

Mockito虽好用,但过度mock会导致测试失去意义。我见过有人把REST API的测试做成了“全链路打桩”,连自己内部Service方法里简单的数据处理都mock掉,最后测试只证明了“Mockito会按你设置的方式返回”,整个测试几乎没有验证价值。

我的实践原则是三层:

第一,如果测试目标是Controller层的参数绑定、序列化、路由、异常处理,就mock掉Service层,专注验证HTTP契约。

第二,如果测试目标是核心业务逻辑,就别用MockMvc了,直接写纯JUnit测试调用Service方法,用内存里的Repository实现或者真实数据库(Testcontainers),确保业务代码真正被执行。

第三,如果测试目标是跨服务的流程编排,那用@SpringBootTest,把模块内部依赖都装配成真的,只mock外部RPC或者消息队列客户端。

这三种测试分层的边界有时候会被混淆,但只要你明确当前测试“要守护什么”,mock的选择自然就清晰了。

6. 排查实录:我在MockMvc测试中踩过的坑

6.1NoSuchMethodError与版本冲突的定位思路

MockMvc + JUnit 5的版本冲突问题非常经典。现象是运行时抛java.lang.NoSuchMethodError,或者java.lang.NoClassDefFoundError。原因多半是Spring框架版本和Spring Boot版本不一致,或者spring-test的版本和spring-webmvc对不上。

我的排查思路是这样的:先看spring-boot-starter-parent定义的版本,再执行mvn dependency:tree检查实际落地的依赖版本。常见的一个坑是项目里自己引了老版本的spring-web,导致MockHttpServletRequest或MockHttpServletResponse里缺失某些方法。

6.2 jsonPath取值一直失败

JsonPath报错最常见的原因是路径写错,比如响应体里字段名是first_name,你写成firstName;或者字段是data而不是data.list。我的经验是调试时先别加太细的jsonPath断言,先用简单的断言打印出响应体内容:

mockMvc.perform(get("/api/books")) .andDo(print());

andDo(print())会把整个请求和响应体打印出来,一眼就能看到实际返回的JSON结构,再对着结构写JsonPath表达式就清晰了。

另外,有些接口返回的JSON里有$符号或者@符号的字段名,比如$ref,JsonPath会把$当作根路径处理,直接取会报错。必须用jsonPath("$['$ref']")这种写法转义。

6.3 MockMvc请求返回404、406、415状态码

这三个状态码在MockMvc测试里是最常出现的,我把它们的经典场景和排查方向列成一个速查表:

状态码典型场景排查方向
404路径不存在或Controller方法未匹配检查URL是否拼错、检查@RequestMapping的路径是否一致、检查测试类是否扫描到了Controller
406Content-Type或Accept头不匹配检查.accept()设置的值,以及@ResponseBody序列化结果是否和Accept匹配
415请求体格式和@RequestBody不匹配检查.contentType()是否设置为APPLICATION_JSON,以及JSON格式是否合法

关于404还有一个坑,就是@WebMvcTest的场景。如果你用@WebMvcTest(BookController.class),但Controller内部通过@RequestMapping直接返回视图名而不是@ResponseBody,就可能出现404。Controller如果在类上加了@Controller而没有加@ResponseBody,方法的返回值会被当作视图名解析,测试接口路径时就会得到404或HttpMessageNotWritableException。解决办法是确保Controller返回的是ResponseEntity,或者类上加了@RestController。

6.4 中文乱码和字符编码问题

中文乱码在MockMvc测试里也经常遇到,但很多时候不是测试代码的问题,而是接口的Content-Type没有包含charset=UTF-8。如果接口返回的JSON是中文,但响应头的Content-Type是application/json而没有charset,MockMvc在解析响应体时可能按ISO-8859-1来处理,导致中文乱码。

一个临时验证方法:在断言前用andDo(print())看打印出来的响应体会不会是乱码。解决方法是让接口侧的MappingJackson2HttpMessageConverter正确配置UTF-8,或者在测试断言时指定字符集:

.andExpect(content().encoding("UTF-8"))

在实际项目中,我会在基类里为所有测试统一配置characterEncoding,防止每个接口测试都重复处理中文编码问题。

6.5 测试数据残留导致断言失败

MockMvc测试如果连了真实数据库(比如H2或者Testcontainers),非常容易出现测试数据残留导致互相影响的问题。比如第一个测试插入了一个id=1的书,第二个测试查询时按id=1能查到,但后续测试断言数量时把之前的数据也算进去了,导致总数对不上。

我长期实践下来的方案是:每个测试方法都开启事务并回滚,或者至少在每个测试之前清理相关数据。Spring的@Transactional用在测试方法上,默认情况下测试结束会回滚,这样测试之间数据不会互相污染。

@SpringBootTest @AutoConfigureMockMvc @Transactional class BookApiIntegrationTest { // ... }

但要注意,使用@Transactional回滚的前提是测试过程中Controller和Service层的数据库操作都在同一个事务里。如果项目中用了REQUIRES_NEW传播行为,或者异步线程里操作数据库,回滚就失效了。这种情况下就得用数据清理策略。

关于这个话题,我在实际项目里还有一个常用的技巧:在测试基类里统一维护一套构造测试数据的工具方法,比如createBookInDb(...),然后在每个测试里用独立的ID范围或数据特征去隔离,断言时也只关心自己创建的数据。

最后分享一点测试重构的经验

MockMvc测试如果不好好控制粒度,项目一大会变得很脆弱。我见过最多的失败原因不是代码逻辑错了,而是测试断言写得过于具体,比如精确断言某个自增ID的具体值,或者断言一个可能因为配置变化而改变的日期格式。所以我的一个原则是:对核心契约(状态码、业务字段、关键业务值)做严格断言,对易变信息(自增ID、服务器时间、分页总数)做存在性和类型断言。

另外,测试命名也值得投入一点精力。别用test1、test2这种名字,我用的是方法名_场景_预期结果的格式,比如createBook_titleTooLong_shouldReturn400。这样的命名在测试报告里一目了然,别人接手你的代码时也能快速知道这个测试在守护什么行为。

REST API的MockMvc + JUnit 5测试说到底是一件需要不断打磨的事。环境配置、请求构造、响应断言,每一层都有细节。等你把前面这些点都跑通了,再回头看那些让新人抓狂的415、406、404,基本就是一眼定位,不会再被它们卡住。

相关新闻

pandas去重实战:drop_duplicates与duplicated参数详解及避坑指南

pandas去重实战:drop_duplicates与duplicated参数详解及避坑指南

我这几年代人做数据清洗&#xff0c;最常听的一句话就是&#xff1a;"把重复的删掉就行。" 说这话的时候大家都很轻松&#xff0c;仿佛重复值是什么一眼就能看穿的东西。可真动手之后才发现&#xff0c; drop_duplicates() 一行代码确实能跑&#xff0c;但跑完之后…

2026/10/20 23:39:26 阅读更多 →
钓鱼攻击防范指南:5类典型邮件诈骗案例与员工安全意识培训要点

钓鱼攻击防范指南:5类典型邮件诈骗案例与员工安全意识培训要点

做网络安全意识培训这几年&#xff0c;我开场一般会先问现场同事一个问题&#xff1a;如果周五下午四点&#xff0c;你的直属领导从企业邮箱发来一封邮件&#xff0c;让你马上安排一笔18.8万的付款&#xff0c;收款信息在附件里&#xff0c;还叮嘱你“合同后补、先付出去”&…

2026/10/20 23:39:26 阅读更多 →
栈与队列:从原理到手写实现,彻底搞懂LIFO和FIFO

栈与队列:从原理到手写实现,彻底搞懂LIFO和FIFO

栈和队列大概是数据结构里最容易被低估的两个东西。很多人学的时候觉得“不就是先进后出和先进先出吗”&#xff0c;然后考试一过就扔了。但真正写过业务代码、调过性能瓶颈的人都知道&#xff0c;这两个结构几乎无处不在&#xff1a;浏览器的前进后退、编辑器的撤销重做、消息…

2026/10/20 23:39:26 阅读更多 →

最新新闻

大模型输出质量忽高忽低?三步自查指南:从会话污染到参数配置

大模型输出质量忽高忽低?三步自查指南:从会话污染到参数配置

1. 从一条吐槽说起&#xff1a;模型输出质量为何会“忽高忽低”最近一段时间&#xff0c;社群里隔三差五就有人冒出来一句&#xff1a;“怎么感觉我的 GPT 变笨了&#xff1f;”有人说是模型被“悄悄降智”了&#xff0c;有人说是自己网络环境的问题&#xff0c;还有人干脆怀疑…

2026/10/21 0:25:44 阅读更多 →
强化学习入门:用Q-learning让机器人走出网格迷宫

强化学习入门:用Q-learning让机器人走出网格迷宫

前两周我帮一个刚接触智能控制的小团队带了个入门 Demo&#xff0c;目标很朴素&#xff1a;让机器人自己走出迷宫&#xff0c;而且不告诉它“哪条路最省事”。迷宫是网格化的&#xff0c;机器人只知道四个方向的动作&#xff0c;撞墙会被扣分&#xff0c;走到出口会拿到奖励&am…

2026/10/21 0:25:44 阅读更多 →
AI重塑教育:从知识图谱到个性化学习的技术落地与实战复盘

AI重塑教育:从知识图谱到个性化学习的技术落地与实战复盘

1. AI 重塑教育的底层逻辑与落地场景这几年我一直在教育技术这个圈子里折腾&#xff0c;见过不少团队拿着大模型产品冲进校园&#xff0c;喊的口号都差不多&#xff1a;"AI 要重塑教育"。但实际跑下来&#xff0c;真正能立住脚的项目&#xff0c;往往不是那些把 AI 吹…

2026/10/21 0:25:44 阅读更多 →
特征向量幅度如何用于图像质量评估:原理与工程实践

特征向量幅度如何用于图像质量评估:原理与工程实践

做图像质量评估的同行&#xff0c;大概都遇到过这种场景&#xff1a;同一个分类模型&#xff0c;喂进去一张很干净的原图&#xff0c;提取出的特征向量“长度”明显比一张压到全是马赛克的模糊图更长。刚开始我以为是偶然现象&#xff0c;后来在几个项目里反复验证&#xff0c;…

2026/10/21 0:25:44 阅读更多 →
AI重塑教育:反馈循环与个性化学习的落地实践

AI重塑教育:反馈循环与个性化学习的落地实践

1. 从“统一节奏”到“千人千面”&#xff1a;AI到底改写了教育的哪个环节大约两年前&#xff0c;我站在一所普通中学的晚自习教室里&#xff0c;看着三十多个学生低头刷题&#xff0c;数学老师一个人抱着一摞作业本来回踱步。那个画面让我突然意识到&#xff1a;教育行业最贵的…

2026/10/21 0:24:35 阅读更多 →
残差流压缩新思路:mHC多头压缩如何让大模型四条流变两条

残差流压缩新思路:mHC多头压缩如何让大模型四条流变两条

最近某通信巨头研究团队放出了一篇关于V4-Flash系列架构的技术报告&#xff0c;通篇最让我印象深刻的不是某个新算子&#xff0c;而是这个结论&#xff1a;在一个原本设计了四条残差流的混合架构里&#xff0c;借助一种叫mHC的多头压缩机制&#xff0c;可以只保留两条流&#x…

2026/10/21 0:23:34 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →