乔人尹 - 代码开发
type: channel
link: https://underestimated.cn/tag/%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91/
items: 谈单元测试怎么写(java篇)
items: Cacheable注解缓存的使用

谈单元测试怎么写(java篇)
type: item
date: 2022-10-16T06:03:00+00:00
description: 本文属于《从入门到放弃》系列，但这次我要劝你别放弃，因为不写单元测试的代码，就像没系安全带的汽车——平时感觉挺自由，出事了直接起飞。前言：一个没写单元测试的程序员的一天早上9点，产品经理说："这个需求很简单，就改一行代码。"你信了，改了。下午3点，线上告警疯狂轰炸你的钉钉。晚上11点，你还在公司排查为什么改了A模块，B模块炸了，C模块也顺便炸了。你开始怀疑人生："我明明只改了一行啊？！"这，就是没有单元测试的代价。如果你写了单元测试，这一行代码改完，跑一下测试，3秒钟就知道有没有搞砸。而不是等到线上用户帮你测试。用户测试的成本，那可是真金白银的赔偿啊兄弟！一、单元测试是什么？不就是写代码测代码吗？没错，单元测试就是用代码测试代码。但很多人对单元测试有误解，以为就是启动整个Spring容器，然后调一下接口看看返回值对不对。那叫集成测试，不叫单元测试。来看一张图，理解一下测试金字塔：graph TB subgraph 测试金字塔 E2E["🔺 E2E测试数量最少 | 成本最高 | 速度最慢"] INT["🔶 集成测试数量适中 | 成本中等 | 速度中等"] UNIT["🟢 单元测试数量最多 | 成本最低 | 速度最快"] end E2E --> INT INT --> UNIT style E2E fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff style INT fill:#feca57,stroke:#333,stroke-width:2px,color:#333 style UNIT fill:#1dd1a1,stroke:#333,stroke-width:2px,color:#fff单元测试的特点：特性说明快毫秒级执行，跑几百个测试也就几秒钟独立不依赖数据库、Redis、MQ等外部服务可重复跑一万次结果都一样自动化CI/CD流水线自动跑单元测试测的是"单元"，在Java里，通常指的是一个方法，最多是一个类。二、Spring Boot测试全家桶Spring Boot贴心地给我们准备了一套测试工具，加个依赖就能用： org.springframework.boot spring-boot-starter-test test 这一个依赖，包含了以下豪华套餐：graph LR subgraph spring-boot-starter-test JUnit5["JUnit 5测试框架本体"] Mockito["MockitoMock神器"] AssertJ["AssertJ断言增强"] Hamcrest["Hamcrest匹配器"] JSONPath["JSONPathJSON断言"] Spring["Spring TestSpring测试支持"] end style JUnit5 fill:#6c5ce7,color:#fff style Mockito fill:#00b894,color:#fff style AssertJ fill:#0984e3,color:#fff style Hamcrest fill:#fdcb6e,color:#333 style JSONPath fill:#e17055,color:#fff style Spring fill:#00cec9,color:#fff2.1 JUnit 5 基础写法先来个最简单的例子热热身：import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @Test @DisplayName("1 + 1 应该等于 2，如果不等于，那数学就完蛋了") void testAdd() { Calculator calculator = new Calculator(); int result = calculator.add(1, 1); assertEquals(2, result); } @Test @DisplayName("除以0应该抛异常，不然宇宙会爆炸") void testDivideByZero() { Calculator calculator = new Calculator(); assertThrows(ArithmeticException.class, () -> { calculator.divide(10, 0); }); } }JUnit 5常用注解速查表：注解作用使用场景@Test标记测试方法每个测试方法都要加@DisplayName给测试起个…

Cacheable注解缓存的使用
type: item
date: 2022-09-18T01:00:00+00:00
description: 前言：一个惨痛的教训话说那年，我写了一个"用户信息查询"接口，代码简洁优雅，自我感觉良好。上线后 QPS 一路飙升，数据库 CPU 直接拉满，DBA 小哥深夜给我打电话，语气中透着一丝疲惫和杀意。"兄弟，你这接口一秒钟查同一个用户几百次，图啥呢？"那一刻，我顿悟了：缓存，是程序员对数据库最基本的尊重。一、@Cacheable 是什么？@Cacheable 是 Spring Cache 提供的声明式缓存注解，属于"我不想写重复代码"系列的终极解决方案。它的核心思想很朴素：第一次调用方法 → 执行方法，结果存缓存后续相同参数调用 → 直接返回缓存，方法体压根不执行简单说就是："这活我干过了，答案在这，别烦我。"二、快速上手2.1 添加依赖 org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-data-redis 2.2 开启缓存功能在启动类上加一个注解，仪式感要有：@SpringBootApplication @EnableCaching // 就这一行，缓存世界的大门向你敞开 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }2.3 使用 @Cacheable@Service public class UserService { @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { // 这行日志是关键，用来验证缓存是否生效 System.out.println(">>> 真的去查数据库了，userId: " + userId); return userRepository.findById(userId).orElse(null); } }测试一下：userService.getUserById(1L); // 控制台输出日志，查了数据库 userService.getUserById(1L); // 静悄悄，缓存命中 userService.getUserById(1L); // 依然静悄悄 userService.getUserById(2L); // 输出日志，新用户要查库恭喜，你的数据库松了一口气。三、核心属性详解@Cacheable 的属性不多，但每个都值得细品。3.1 value / cacheNames —— 缓存的"收纳盒"@Cacheable(value = "userCache") @Cacheable(cacheNames = "userCache") // 两种写法等价 @Cacheable(value = {"cache1", "cache2"}) // 可以同时放多个盒子这玩意就是给缓存分类，就像你家的收纳盒：袜子放一个、内裤放一个，别混了。3.2 key —— 缓存的"门牌号"默认情况下，Spring 会用方法的所有参数生成 key。但很多时候我们需要自定义：// 使用 SpEL 表达式 @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { ... } // 多参数组合 @Cacheable(value = "userCache", key = "#userId + '_' + #type") public User getUserByIdAndType(Long userId, String type) { ... } // 使用对象的属性 @Cacheable(value = "userCache", key = "#user.id") public User updateUser(User user) { ... } // 使用方法名 + 参数（防止不同方法 key 冲突） @Cacheable(value = "userCache", key = "'getUser_' + #userId") public User getUserById(Long userId) { ... }踩坑预警：key 冲突是缓存的头号杀手。想象一下，getUserById(1L) 和 getOrderById(1L) 用了同一个 key，返回数据直接串台，这 bug 够你查三天。3.3 condition —— 有条件地缓存不是所有数据都值得缓存，…