一、一句话区分
| 单元测试 | 手工测试 | |
|---|---|---|
| 谁来跑 | 机器(JUnit/Mockito) | 人 |
| 测什么 | 代码逻辑对不对 | 用户体验对不对 |
| 多快 | 秒级 | 分钟~小时级 |
| 能否进 CI | ✅ 每次提交自动跑 | ❌ 需要人手操作 |
二、水印功能的测试全景
我们的水印模块涉及三个层面:路径解析 → 图片绘制 → 权限分流。项目里为每个层面写了对应的单元测试,共 3 个测试类、12 个用例,全部是纯内存测试,不连 MySQL、Redis、S3。
项目结构
src/test/java/.../upload/watermark/
├── S3KeyUtilsTest.java # S3 路径解析(6 个用例)
├── ImageWatermarkServiceTest.java # 水印绘制(2 个用例)
└── ListingPhotoAccessServiceImplTest.java # 权限分流(4 个用例)
三、单元测试长什么样
3.1 S3KeyUtilsTest --- 路径映射
测什么: URL ↔ S3 key 之间的转换规则,纯工具类,无任何 Mock。
java
// 示例:验证新上传路径的水印 key 映射
@Test
void resolveWatermarkedKey_forNewUploadLayout() {
// listings/original/abc.jpg → listings/watermarked/abc.jpg
String result = S3KeyUtils.resolveWatermarkedKey("listings/original/abc.jpg");
assertEquals("listings/watermarked/abc.jpg", result);
}
特点:
- 输入明确,输出可预测
- 不依赖外部系统
- 跑完 6 个用例不到 1 秒
3.2 ImageWatermarkServiceTest --- 水印绘制
测什么: Java2D 在内存中画水印,不访问 S3、不访问网络。
java
@Test
void applyWatermark_keepsImageDimensions() {
BufferedImage source = new BufferedImage(800, 600, TYPE_INT_RGB);
BufferedImage result = service.applyWatermark(source, context);
assertEquals(800, result.getWidth()); // 尺寸不变
assertEquals(600, result.getHeight());
}
@Test
void applyWatermark_outputsJpegBytes() {
byte[] pngBytes = encodePng(source);
byte[] watermarked = service.applyWatermark(pngBytes, context);
assertNotNull(watermarked);
assertTrue(watermarked.length > 0);
// 验证输出是合法 JPEG
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(watermarked));
assertNotNull(decoded);
}
特点:
- 手动
new ImageWatermarkService(...),不走 Spring 容器 - 用空白
BufferedImage当输入,classpath 下的 PNG 当 Logo - 只验证"能生成合法 JPEG",不验证 Logo 位置、文字内容------那些需要人眼确认
3.3 ListingPhotoAccessServiceImplTest --- 权限分流
测什么: 根据用户身份决定返回原图还是水印 URL。
java
@Test
void resolvePhotoUrls_ownerGetsOriginal() {
// 用户 100 看自己的房源 → 返回原图 URL
List<String> urls = service.resolvePhotoUrls(listingPhotos, 100L);
assertTrue(urls.get(0).contains("original"));
}
@Test
void resolvePhotoUrls_otherUserGetsWatermarked() {
// 用户 200 看别人的房源 → 返回水印 URL
List<String> urls = service.resolvePhotoUrls(listingPhotos, 200L);
verify(watermarkService).ensureWatermarked(any());
}
特点:
- 用 Mockito
@Mock模拟依赖,不启动 Spring Boot - 覆盖四种场景:业主、他人、助手、分享链接
- 验证的是业务规则,不关心水印本身长什么样
四、手工测试测什么
单元测试覆盖不到的东西,就需要人来做:
| 测试项 | 为什么单元测试做不了 |
|---|---|
| S3 真实上传/下载 | 依赖 AWS 网络,Mock 无法替代真实网络延迟和权限 |
| Controller HTTP 接口 | 需要完整请求链路:Filter → Interceptor → Controller → Service → S3 |
| 水印视觉效果 | Logo 位置对不对?平铺文字是否清晰?"Listed by" 条颜色对不对?这些只能靠人眼 |
手工测试典型流程:
- 启动应用,打开 Swagger UI 或 Postman
- 调上传接口传一张真实房源图片
- 调下载接口,分别用业主身份和访客身份
- 打开下载的 jpg,肉眼确认:
- ✅ Logo 在右下角
- ✅ 半透明平铺文字覆盖全图
- ✅ "Listed by xxx" 底部条清晰可读
- ✅ 水印图尺寸和原图一致
五、核心区别对照表
| 维度 | 单元测试 | 手工测试 |
|---|---|---|
| 执行速度 | 秒级(12 个用例 < 3s) | 分钟~小时级 |
| 可重复性 | 完全相同输入 → 完全相同结果 | 人可能漏看、误判 |
| 依赖 | 不连 MySQL/Redis/S3 | 需要完整运行环境 |
| CI 集成 | ✅ 每次 push 自动跑 | ❌ 无法自动化 |
| 反馈时机 | 写代码时立刻知道对不对 | 部署后才能验证 |
| 测什么 | 逻辑正确性 | 视觉体验、真实链路 |
| 维护成本 | 代码即文档,改逻辑时同步改 | 需要写测试用例文档 |
| 发现问题类型 | 边界条件、回归 bug | 视觉瑕疵、交互问题 |
六、一个形象的类比
单元测试 = 工厂质检机器
手工测试 = 质检员肉眼验收
机器能测:
✅ 螺丝扭矩是否达标
✅ 零件尺寸是否在公差范围内
✅ 电路是否导通
质检员看:
👀 漆面有没有划痕
👀 屏幕颜色是否偏色
👀 整体手感是否舒适
两种方式互补,缺一不可。
七、项目当前测试分层总结
┌─────────────────────────────────────────┐
│ 手工测试 / 视觉验收 │ ← 人眼:Logo位置、文字、S3真实链路
├─────────────────────────────────────────┤
│ 集成测试(未实现) │ ← @SpringBootTest + 真实 S3/DB
├─────────────────────────────────────────┤
│ Controller 层测试(未实现) │ ← @WebMvcTest + MockMvc
├─────────────────────────────────────────┤
│ Service 层单元测试 ✅ 已实现(3类12例) │ ← Mockito,纯内存
├─────────────────────────────────────────┤
│ Util 工具类测试 ✅ 已实现 │ ← 纯 JUnit,无依赖
└─────────────────────────────────────────┘
八、总结
- 单元测试验证代码逻辑------快、可重复、能进 CI,适合覆盖边界条件和回归场景。
- 手工测试验证用户体验------能看到真实效果,发现代码无法自证的视觉问题。
- 二者不是替代关系,而是互补关系。 单元测试保证"没写错",手工测试保证"没做错"。
- 当前项目的水印单元测试覆盖了 路径解析、绘制基础、权限分流 三个核心逻辑层。S3 读写、HTTP 接口、视觉效果仍需手工或后续集成测试覆盖。