MyBatis-Plus 单元测试中 Lambda Mock 的坑与解决

文章目录

  • [MyBatis-Plus 单元测试中 Lambda Mock 的坑与解决](#MyBatis-Plus 单元测试中 Lambda Mock 的坑与解决)
    • 一、问题现象:一个很"莫名其妙"的异常
    • 二、先给结论(重要)
    • [三、问题根源:Lambda 缓存 + Mock = 翻车现场](#三、问题根源:Lambda 缓存 + Mock = 翻车现场)
      • [1️⃣ MyBatis-Plus 的 Lambda 是"有状态"的](#1️⃣ MyBatis-Plus 的 Lambda 是“有状态”的)
      • [2️⃣ 单元测试里的 Mapper 是"假的"](#2️⃣ 单元测试里的 Mapper 是“假的”)
    • [四、最小 Demo:错误 vs 正确](#四、最小 Demo:错误 vs 正确)
      • [❌ 错误示例(非常常见)(高概率翻车)](#❌ 错误示例(非常常见)(高概率翻车))
      • [✅ 实测最稳方案(推荐)](#✅ 实测最稳方案(推荐))
    • [五、一个重要认知:单元测试不该关心 SQL 细节](#五、一个重要认知:单元测试不该关心 SQL 细节)
      • [❓ 单元测试应该测什么?](#❓ 单元测试应该测什么?)
      • [❌ 不应该在单元测试里测什么?](#❌ 不应该在单元测试里测什么?)
    • [六、如果"我就是想用 Lambda",怎么办?](#六、如果“我就是想用 Lambda”,怎么办?)
      • [方案:手动触发 Lambda 缓存初始化](#方案:手动触发 Lambda 缓存初始化)
    • 七、真实项目中的推荐测试分层
    • 八、顺手避坑:UnnecessaryStubbingException
      • [❌ 常见反例](#❌ 常见反例)
      • [✅ 正确做法:按需 Mock](#✅ 正确做法:按需 Mock)
    • [九、总结(请记住这 3 句话)](#九、总结(请记住这 3 句话))

MyBatis-Plus 单元测试中 Lambda Mock 的坑与解决

核心主题

单元测试里 Mock MyBatis-Plus Mapper 时,为什么一用 LambdaQueryWrapper / LambdaUpdateWrapper 就报错?

以及:如何用最简单、最稳妥的方式解决它。


一、问题现象:一个很"莫名其妙"的异常

很多人在写 MyBatis-Plus 单元测试时,都见过下面这个异常:

text 复制代码
MybatisPlusException:
can not find lambda cache for this entity [com.example.domain.User]

它通常出现在:

  • 使用 LambdaQueryWrapper
  • 使用 LambdaUpdateWrapper
  • Mapper 被 Mockito @Mock@MockBean

线上环境完全没问题,只有单元测试会炸

这类问题非常典型,也非常让人困惑。


二、先给结论(重要)

99% 的场景下,这不是你业务代码的问题,而是 Mock 方式不当。

最稳妥、最推荐的做法只有一句:⚠️ 折中方案(不完全可靠)

java 复制代码
when(userMapper.update(any(), any())).thenReturn(1);
  • 在很多项目中可用
  • 但在某些 MyBatis-Plus 版本 / 复杂测试场景下
  • 仍可能失败

而不是:

java 复制代码
// ❌ 非常容易触发 Lambda cache 异常
when(userMapper.update(any(User.class), any(LambdaUpdateWrapper.class))).thenReturn(1);

下面我们一步一步解释:为什么会这样,以及为什么 any() 能救命。


三、问题根源:Lambda 缓存 + Mock = 翻车现场

1️⃣ MyBatis-Plus 的 Lambda 是"有状态"的

MyBatis-Plus 的 Lambda 写法并不是语法糖,它背后依赖一个 Lambda 缓存

  • 通过 User::getId 解析出字段名 id
  • 解析结果会缓存在内部 LambdaCache
  • 这个缓存和 实体类的 Class、类加载器、初始化时机 强相关

一句话总结:

LambdaWrapper 不是一个普通 POJO,它在构造时就依赖 MyBatis-Plus 的运行环境。


2️⃣ 单元测试里的 Mapper 是"假的"

在单元测试中我们通常这样写:

java 复制代码
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserMapper userMapper;
}

此时:

  • Mapper 是 Mockito 生成的代理对象
  • 不存在真实的 MyBatis 上下文
  • Lambda 缓存可能根本没初始化

但你却在 Mock 时强行写:

java 复制代码
any(LambdaUpdateWrapper.class)

👉 Mockito 会尝试去解析这个参数类型

👉 MyBatis-Plus 会尝试读取 Lambda 缓存

👉 然后发现:缓存不存在

于是,异常就出现了。


四、最小 Demo:错误 vs 正确

❌ 错误示例(非常常见)(高概率翻车)

java 复制代码
@Test
void testDeleteUser() {
    when(userMapper.update(any(User.class), any(LambdaUpdateWrapper.class)))
            .thenReturn(1);

    boolean result = userService.deleteUser(1L);
    assertTrue(result);
}

问题点只有一个:

你在单元测试里,强依赖了 LambdaUpdateWrapper 的内部机制。


✅ 实测最稳方案(推荐)

单元测试中:彻底避免 LambdaWrapper。

业务代码调整(关键)
java 复制代码
// ❌ 原写法
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId);

// ✅ 推荐写法
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", userId);
对应单元测试
java 复制代码
@Test
@DisplayName("删除用户 - 成功")
void testDeleteUser() {
    when(userMapper.update(any(), any()))
            .thenReturn(1);

    boolean result = userService.deleteUser(1L);

    assertTrue(result);
}

为什么它最稳?

  • QueryWrapper / UpdateWrapper 是普通对象
  • 不依赖 Lambda 缓存
  • 不依赖实体元数据初始化
  • 与 Mockito / 测试环境完全解耦

👉 *这是目前实践中稳定性最高、成本最低的方案。


五、一个重要认知:单元测试不该关心 SQL 细节

很多 Lambda Mock 问题,本质是测试关注点放错了

❓ 单元测试应该测什么?

  • 业务逻辑是否正确
  • 条件成立时是否调用了 Mapper
  • 返回值是否被正确处理

❌ 不应该在单元测试里测什么?

  • LambdaWrapper 拼了哪些字段
  • SQL 是否正确
  • 条件构造是否符合预期

👉 这些是集成测试 / Mapper 测试的职责。


六、如果"我就是想用 Lambda",怎么办?

有,但不推荐。

方案:手动触发 Lambda 缓存初始化

java 复制代码
@Test
void testWithLambdaInit() {
    // 触发一次 Lambda 解析
    new LambdaQueryWrapper<User>().select(User::getId);

    when(userMapper.selectList(any())).thenReturn(List.of());

    List<User> users = userService.list();
    assertNotNull(users);
}

缺点非常明显:

  • 测试代码变脏
  • 可读性下降
  • 仍可能受类加载器影响

👉 只在极少数必须验证 Lambda 行为的场景下使用。


七、真实项目中的推荐测试分层

text 复制代码
单元测试(Service)
  - Mock Mapper
  - 一律 any()
  - 只测业务分支

集成测试(Mapper / DB)
  - 真数据库 / Testcontainers
  - 真 LambdaWrapper
  - 验证 SQL 行为

不要试图在一个测试里干两件事。


八、顺手避坑:UnnecessaryStubbingException

❌ 常见反例

java 复制代码
@BeforeEach
void setUp() {
    when(cryptoKeyProvider.getMasterKey()).thenReturn(key);
}

但某些测试根本用不到它。


✅ 正确做法:按需 Mock

java 复制代码
@Test
void testCreateUser() {
    when(cryptoKeyProvider.getMasterKey()).thenReturn(key);
    when(userMapper.insert(any())).thenReturn(1);
}

👉 Mock 写在测试里,而不是统一写在 @BeforeEach


九、总结(请记住这 3 句话)

  1. LambdaWrapper 在单元测试里是"高危对象"
  2. Service 单测中,any() 是最优解
  3. SQL 正确性不属于单元测试的职责

如果你在单测里看到:

text 复制代码
can not find lambda cache

别急着怀疑人生,先看看你是不是写了:

java 复制代码
any(LambdaUpdateWrapper.class)

改成 any(),世界大概率就清净了。

相关推荐
测试员周周5 小时前
【AI测试智能体-面试】AI测试面试60题(附回答思路)
人工智能·python·功能测试·测试工具·单元测试·自动化·测试用例
SuperArc19999 小时前
SpringBoot+Slf4j+Log4j2+mybatis 日志整合
spring boot·mybatis·log4j2·slf4j·日志整合
可乐ea12 小时前
【Spring Boot + MyBatis|第4篇】MyBatis 动态 SQL:if、where、foreach 使用详解
java·spring boot·后端·sql·mybatis
kTR2hD1qb12 小时前
Privaze源码级避坑指南技术文章大纲
log4j
阿正的梦工坊13 小时前
【Rust】10-Cargo、测试与实用开发工作流
java·rust·log4j
摇滚侠15 小时前
Spring 零基础入门到进阶 单元测试 JUnit 52-60
spring·junit·单元测试
AI thought15 小时前
C语言企业项目实战(四)
c语言·单元测试·压力测试·企业项目·工程体系
一条泥憨鱼15 小时前
苍穹外卖【day5|Redis与店铺营业状态设置】
java·后端·mybatis·苍穹外卖
来杯@Java1 天前
学生选课管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·maven·mybatis
技术落地手记1 天前
一个需求 ID 换一份完整测试用例,我让 AI 替测试同事省掉半天
单元测试·测试