拼豆制图已经包含资源字符解码、70×70 像素量化、2×2 主色降采样、加权颜色距离、图例行数和稳定 ID 解析等逻辑,但测试目录仍处于工程初始化状态:本地测试只有一个assertContain用例,设备侧测试也只有一个同名示例,两者都在验证字符串abc包含b。
这不是说应用逻辑无法验证,而是当前很多算法被写成服务或页面里的private static、private方法,测试入口尚未形成。本篇以真实现状为起点,设计一条可执行的升级路线:先提取不依赖系统能力的纯函数,再用边界样本覆盖字符、透明度、颜色距离、分组和图例几何;PixelMap、相册选择与媒体保存继续留在设备侧。文中的新增测试是建议实现,不会把尚未执行的结果写成既成事实。
一、先盘点当前到底有什么测试
本地目录的唯一用例是:
it('assertContain',0,()=>{leta='abc';letb='b';expect(a).assertContain(b);expect(a).assertEqual(a);});设备侧Ability.test.ets也只有一个结构相同的用例,额外输出一条hilog。因此当前基线可以准确描述为:
本地测试:1 个 it,2 个通用字符串断言 设备测试:1 个 it,2 个通用字符串断言 业务算法断言:0明确基线很重要。只有知道当前是 0 个业务断言,后续新增的每个用例才有清晰价值,也不会把示例代码误当成已有保障。
二、不要先测试页面,先把纯计算从 private 中移出来
当前高价值算法分散在三个位置:
| 位置 | 典型逻辑 | 当前可直接导入 |
|---|---|---|
PatternRepository | 字符索引、统计、变体解析 | 否,核心方法为 private |
ImageConvertService | 白底混合、颜色距离、主色降采样 | 否,核心方法为 private static |
Index | 格子分行、当前图案解析 | 否,页面私有方法且带状态 |
测试不应通过修改可见性去调用页面内部细节,也不应为了验证一个公式启动整页。更稳妥的第一步是提取无副作用模块,例如:
// common/PatternAlgorithms.etsexportfunctionassetColorIndex(token:string):number{/* ... */}exportfunctionlegendRows(colorCount:number,columns:number):number{/* ... */}exportfunctionblendWithWhite(value:number,alpha:number):number{/* ... */}exportfunctionweightedColorDistance(a:Rgb,b:Rgb):number{/* ... */}exportfunctionsplitCells<T>(cells:T[],width:number):T[][]{/* ... */}服务层调用这些函数,测试也导入同一份实现。这样测到的是生产代码,而不是复制一遍公式后验证副本。
三、第一组:字符解码要覆盖合法区间和非法输入
资源编码使用.、0-9和A-F。最小边界集应包含:
it('decodes asset tokens',0,()=>{expect(assetColorIndex('.')).assertEqual(-1);expect(assetColorIndex('0')).assertEqual(0);expect(assetColorIndex('9')).assertEqual(9);expect(assetColorIndex('A')).assertEqual(10);expect(assetColorIndex('F')).assertEqual(15);expect(assetColorIndex('G')).assertEqual(-1);expect(assetColorIndex('a')).assertEqual(-1);});A和F分别覆盖字母区间的首尾,G覆盖上界之外,小写a则确认协议区分大小写。仅测试0或A都无法证明边界正确。
还应为整张资源增加结构扫描:70 行、每行 70 字符、色板不超过 16 色、所有非点字符都能映射到色板范围。字符函数测试负责映射规则,资源扫描负责实际数据,两者不能互相替代。
四、第二组:图例行数必须盯住 4 到 5 的跳变
四列图例的公式是:
exportfunctionlegendRows(colorCount:number,columns:number):number{returnMath.max(1,Math.ceil(colorCount/columns));}建议样本:
it('calculates four-column legend rows',0,()=>{expect(legendRows(0,4)).assertEqual(1);expect(legendRows(4,4)).assertEqual(1);expect(legendRows(5,4)).assertEqual(2);expect(legendRows(16,4)).assertEqual(4);});4 -> 1与5 -> 2是最重要的一对。如果有人把Math.ceil改成Math.floor,16 色仍然会得到 4,只有 5 色样本能立即暴露裁切风险。
同时要决定非法列数如何处理。columns=0会产生无穷值,不能默默进入尺寸计算。纯函数应在入口返回错误或抛出明确异常,并为0与负数各留一个用例。
五、第三组:透明度阈值要同时覆盖 15 和 16
像素转换中,alpha 小于 16 的像素被视为空格:
if(alpha<16){returnemptyCell;}因此必须成对验证:
alpha = 15 -> 空格 alpha = 16 -> 先与白色混合,再寻找最近色白底混合公式为:
Math.round((value*alpha+255*(255-alpha))/255)固定样本可直接复算:
expect(blendWithWhite(10,255)).assertEqual(10);expect(blendWithWhite(10,0)).assertEqual(255);expect(blendWithWhite(10,128)).assertEqual(132);这三个值分别覆盖完全不透明、完全透明和中间透明度。随机选择几个 alpha 虽然用例更多,却不如边界值容易解释。
六、第四组:加权颜色距离不能只测试“相同颜色为零”
工程使用带红色均值修正的加权平方距离:
constredMean=(redA+redB)/2;return(2+redMean/256)*redDiff*redDiff+4*greenDiff*greenDiff+(2+(255-redMean)/256)*blueDiff*blueDiff;至少应验证四种性质:
- 同色距离为 0。
- 交换 A、B 后结果相同。
- 绿色通道差 10 的贡献为 400。
- 输入靠近不同红色均值时,红蓝权重按公式变化。
示例:
consta={red:40,green:80,blue:120};constb={red:50,green:80,blue:120};expect(distance(a,b)).assertEqual(distance(b,a));expect(distance(a,a)).assertEqual(0);浮点结果不宜一律用整数相等,可以比较绝对误差是否小于固定阈值。真正要防的是权重项、通道顺序或平方被误删。
七、第五组:2×2 主色降采样要固定平票规则
70×70 图生成 35×35 预览时,每个预览格读取一个 2×2 区块。dominantColorInBlock()统计各色出现次数,并只在count > bestCount时替换最佳项。
这意味着平票时保留色板索引更小的颜色。建议覆盖:
[A, A, B, empty] -> A [A, B, A, B] -> A,平票按色板顺序 [empty × 4] -> null [B, B, B, A] -> B平票行为必须写进测试名称,否则未来有人把>改为>=,结果会从“前者优先”变成“后者优先”,预览图会在大量 2:2 区块中出现不稳定色点。
由于当前主色函数依赖私有色板,可进一步把它改为接收paletteIds或已解析索引,让纯函数不再读取服务的静态状态。
八、第六组:格子分行要先挡住 width=0
页面中的分行逻辑为:
for(leti=0;i<cells.length;i+=width){rows.push(cells.slice(i,i+width));}正常输入cells.length=4900、width=70会得到 70 行。但若width=0且 cells 非空,i += 0会让循环永远无法前进。这正是纯函数测试能提前捕获的危险边界。
提取后的入口应先保护:
if(width<=0){thrownewError('width must be greater than zero');}随后覆盖:
空数组 + width 70 -> 0 行 5 个格子 + width 2 -> [2, 2, 1] 4900 个格子 + width 70 -> 70 行,每行 70 非空数组 + width 0 -> 明确失败,不进入循环尾行[2,2,1]可以证明slice不会丢掉不足一整行的数据;资源图则应在更早的结构校验中保证整除。
九、统计函数要同时验证“色板颜色”和“可见颜色”
countColors()会为色板中的每个颜色都生成统计项,即使其计数为 0;visibleColorCount()才统计count > 0的项。用例应明确两种语义:
palette.length = 3 cells 使用 A 两次、B 一次、C 零次 colorStats.length = 3 visibleColorCount = 2 countBeads = 3如果只断言colorStats.length,无法发现计数内容错误;如果只断言可见颜色数,又可能漏掉零计数项是否被保留。图例布局使用哪个数,也应通过调用方用例明确下来。
十、本地测试与设备侧测试的边界
并不是所有逻辑都应该塞进本地测试。可以按依赖分三层:
本地 Hypium
- 字符索引与资源结构扫描。
- 图例行数和画布尺寸算式。
- alpha 白底混合、RGB 解析、颜色距离。
- 2×2 主色选择、颜色统计、格子分行。
- 种子 ID、顺序、热度与变体固定样本。
设备侧 ohosTest
PixelMap.readPixelsToBuffer()得到的 RGBA 通道顺序。- 图片解码、缩放和释放时机。
- 相册选择器的成功与取消分支。
- 媒体保存对话框与临时文件流程。
- PNG 打包器输入输出是否被系统组件接受。
人工图像复核
- 70×70 网格的十格粗线。
- 三位色号是否溢出。
- 16 色图例与页脚是否重叠。
- 相册中实际保存文件的清晰度与方向。
本地测试快且定位精确,设备侧验证系统契约,人工复核负责视觉结果。三层回答的问题不同,不能用一张成功截图代替算法边界,也不能用纯函数断言声称系统相册已经可用。
十一、测试名称应该直接描述输入与规则
与其继续使用assertContain,更有价值的名称是:
assetColorIndex_returns15_forF legendRows_movesToSecondRowAtFiveColors alpha15_isEmpty_but_alpha16_isQuantized dominantColor_keepsLowerPaletteIndexOnTie splitCells_rejectsZeroWidth seedAnime6_reusesVariantZeroWithoutChangingId失败报告不打开测试源码就能看出是哪条契约变化。名称里包含边界值,也能阻止后来者用更“普通”的样本替换掉关键输入。
十二、从 0 个业务断言开始的落地顺序
建议按风险与改造成本逐步推进:
- 新建纯算法文件,先迁移字符索引、图例行数、白底混合和颜色距离。
- 为每个函数补首尾、跳变与非法输入。
- 迁移格子分行并增加
width <= 0保护。 - 把主色选择改成显式输入色板,固定平票规则。
- 增加 50 张资源的全量结构扫描。
- 最后再补 PixelMap、相册选择和媒体保存的设备侧用例。
每一步都应先让生产代码调用新函数,再删除旧私有实现,避免两份算法长期并存。完成后记录真实执行环境与结果;未执行的设备路径仍然明确标注待验证。
小结
Harmony os 项目的测试升级不应从“给页面多点几次”开始。当前工程只有两个通用示例用例,业务断言为 0,最有价值的第一步是把字符解码、图例几何、alpha 混合、颜色距离、主色降采样和格子分行提取为可导入的纯函数。
边界样本才是这张回归网的亮点:4 到 5 色决定图例是否换行,alpha 15 与 16 决定格子是否为空,2:2 平票决定预览颜色,width 0 决定循环能否结束。把这些真实分界固定下来,再把 PixelMap 与相册能力留给设备侧,测试就能从占位示例变成能够解释故障位置的工程资产。