test(unit): 三个纯函数的最小单测(12 条)—— 本仓库第一批单测(F-9)
后端验证一直靠 E2E(打真实接口,很硬但要起服务连库)。这批先给 「输入输出确定、没有 IO」的纯函数补最小单测,成本最低、回归收益最高; 其余继续靠 E2E,不为了覆盖率硬凑。 · IdentityNormalizerTest:姓名去所有空白但**保留间隔号**(少数民族姓名的 「·」是真实的,顺手清掉等于改人名字)、全角转半角、证件号小写 x 收尾统一、 null 不抛 NPE · FileProcessServiceTest:扩展名必须自带点号(少一个字符不报错,只会产出 「身份证jpg」这种客户双击打不开的文件 —— F-46)、空变量用「未填」占位 (留空会出现「张--身份证.jpg」)、文件名里的 / 必须清掉(「学籍表(1/2)」 带 / 根本写不进磁盘)、超长截到 90 且不吃掉扩展名、sanitize 收拢连续分隔符 每条断言都对应一个真实踩过的坑 —— 断言的是「有理由的规则」,不是覆盖率。 验证:mvn test → 12/12 通过(exit 0)
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package cn.tianbaotong.fill.module.file.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范文件名引擎的最小单测(《08》F-9 / F-46)。
|
||||||
|
*
|
||||||
|
* <p>断言的是<b>有理由的规则</b>,不是覆盖率:
|
||||||
|
* <ul>
|
||||||
|
* <li>扩展名必须自带点号(F-46:少一个字符不报错,只会产出「身份证jpg」这种
|
||||||
|
* 客户双击打不开的文件);</li>
|
||||||
|
* <li>空变量用「未填」占位,而不是留空 —— 留空会出现「张同学--身份证.jpg」,
|
||||||
|
* 看起来像系统出错;</li>
|
||||||
|
* <li>文件名里的 {@code /} 必须被清掉(学生的「学籍表(1/2)」很常见,
|
||||||
|
* 带 / 的名字根本写不进磁盘);</li>
|
||||||
|
* <li>超长截断 —— Windows 路径 260 限制,目录还要留空间。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
class FileProcessServiceTest {
|
||||||
|
|
||||||
|
private final FileProcessService svc = new FileProcessService();
|
||||||
|
|
||||||
|
private static Map<String, String> vars(String name, String id, String material) {
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
m.put("姓名", name);
|
||||||
|
m.put("证件号", id);
|
||||||
|
m.put("材料名", material);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 默认模板_姓名证件号材料名_扩展名带点() {
|
||||||
|
String n = svc.renderName(null, vars("张三", "361029200907295110", "身份证"), ".jpg");
|
||||||
|
assertEquals("张三-361029200907295110-身份证.jpg", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 模板带后缀占位符时不重复拼扩展名() {
|
||||||
|
String n = svc.renderName("{材料名}-{姓名}{后缀}",
|
||||||
|
vars("张三", "361029200907295110", "身份证"), ".jpg");
|
||||||
|
assertEquals("身份证-张三.jpg", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 空变量用未填占位_不会出现双横线() {
|
||||||
|
String n = svc.renderName(null, vars("张三", "", "身份证"), ".jpg");
|
||||||
|
assertEquals("张三-未填-身份证.jpg", n);
|
||||||
|
assertFalse(n.contains("--"), "留空会出现「张--身份证.jpg」,看起来像系统出错: " + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 文件名里的斜杠必须被清掉() {
|
||||||
|
// 「学籍表(1/2)」在真实学生的上传里很常见;带 / 的名字写不进磁盘
|
||||||
|
String n = svc.renderName(null, vars("张三", "361029200907295110", "学籍表(1/2)"), ".jpg");
|
||||||
|
assertFalse(n.contains("/"), "斜杠会导致写盘失败: " + n);
|
||||||
|
assertTrue(n.contains("学籍表(1_2)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 超长文件名截到安全长度() {
|
||||||
|
Map<String, String> m = vars("张".repeat(120), "361029200907295110", "身份证");
|
||||||
|
String n = svc.renderName(null, m, ".jpg");
|
||||||
|
assertTrue(n.length() <= 90, "Windows 路径 260 限制,正文要给目录留空间: " + n.length());
|
||||||
|
assertTrue(n.endsWith(".jpg"), "截断不能吃掉扩展名");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sanitize_空白给未命名_全非法字符收拢成单个下划线() {
|
||||||
|
// "///" → "___" → 收拢成 "_"(不是「未命名」:未命名只留给纯空白,
|
||||||
|
// 全非法字符收拢后仍是可写盘的合法名字)
|
||||||
|
assertEquals("_", FileProcessService.sanitize("///"));
|
||||||
|
assertEquals("未命名", FileProcessService.sanitize(" "));
|
||||||
|
assertEquals(null, FileProcessService.sanitize(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sanitize_连续分隔符收拢() {
|
||||||
|
assertEquals("a_b", FileProcessService.sanitize("a___b"));
|
||||||
|
assertEquals("a-b", FileProcessService.sanitize("a---b"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package cn.tianbaotong.fill.support;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link IdentityNormalizer} 的最小单测(《08》F-9:先给纯函数补单测,其余继续靠 E2E)。
|
||||||
|
*
|
||||||
|
* <p>选这三条规则断言,是因为每一条背后都有一个<b>真实踩过的坑</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>姓名去空白、但<b>不去间隔号</b> —— 少数民族姓名里的「·」是真实的;
|
||||||
|
* 顺手"清一清"会弄坏一部分人的名字(F-20 的另一面:规范化规则不能互相借用);</li>
|
||||||
|
* <li>全角转半角 —— 手机输入法常给全角数字,而它进校验位算法就是错的;</li>
|
||||||
|
* <li>证件号小写 {@code x} 收尾要统一成大写 —— 名册比对按字符串等值。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
class IdentityNormalizerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 姓名去所有空白_含全角空格() {
|
||||||
|
assertEquals("张三", IdentityNormalizer.name("张 三"));
|
||||||
|
assertEquals("张三", IdentityNormalizer.name("张 三")); // 全角空格 U+3000
|
||||||
|
assertEquals("张三", IdentityNormalizer.name(" 张\t三\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 姓名里的间隔号必须保留() {
|
||||||
|
// 阿依古丽·买买提:间隔号是姓名的一部分,清掉等于改了人家的名字
|
||||||
|
assertEquals("阿依古丽·买买提", IdentityNormalizer.name("阿依古丽·买买提"));
|
||||||
|
assertEquals("阿依古丽·买买提", IdentityNormalizer.name("阿依古丽 · 买买提"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 全角ASCII转半角_其余不动() {
|
||||||
|
assertEquals("123X", IdentityNormalizer.toHalfWidth("123X"));
|
||||||
|
assertEquals("中文不变", IdentityNormalizer.toHalfWidth("中文不变"));
|
||||||
|
assertEquals("", IdentityNormalizer.toHalfWidth(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 证件号去空白_转大写_全角可入() {
|
||||||
|
assertEquals("36102920090729511X", IdentityNormalizer.idCard("3610292009 0729511x"));
|
||||||
|
assertEquals("36102920090729511X", IdentityNormalizer.idCard("36102920090729511X"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 姓名空值返回空串而不是NPE() {
|
||||||
|
assertEquals("", IdentityNormalizer.name(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user