基于 UFS 测试工具骨架继续演进:如何扩展测试项、接入 SDK、做成可落地工程

上一篇我介绍了这个 UFS 测试工具 的整体骨架:

  • WinForms 负责界面承载
  • IUfsAdapter 隔离底层厂商实现
  • UfsService 负责会话和业务封装
  • ITestCase + TestRunner 组成可扩展测试引擎
  • TestController + ViewModel 负责测试流程和界面协作

这一篇我不再重复讲分层,而是重点聊:这套骨架接下来应该怎么长,才能从 Demo 骨架变成真正能持续演进的工程工具。


一、骨架已经解决了什么问题?

很多人看到"骨架"两个字,会觉得它只是代码拆得更漂亮一些。

但对测试工具来说,骨架真正解决的是"后续修改成本"。

当前这套结构,已经先把最容易失控的几个点卡住了:

  • 厂商差异被隔离 :底层都收敛到 IUfsAdapter
  • 测试项可插拔 :新增用例只要实现 ITestCase
  • 会话统一管理 :由 TestRunner 负责打开和关闭设备
  • 取消链路已打通 :从 UI 到测试执行可以传递 CancellationToken
  • 结果对象统一 :所有测试最终都回到 TestResult

这意味着后面的迭代,不需要再推翻已有结构,而是在现有边界上不断补能力。

如果要继续演进,我觉得有两张图值得反复看,因为它们直接决定了后面应该优先补什么。

首先是依赖注入关系图:
#mermaid-svg-nx5yTe9Ib5mdLmkW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nx5yTe9Ib5mdLmkW .error-icon{fill:#552222;}#mermaid-svg-nx5yTe9Ib5mdLmkW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nx5yTe9Ib5mdLmkW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .marker.cross{stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nx5yTe9Ib5mdLmkW p{margin:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label text{fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label span{color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label span p{background-color:transparent;}#mermaid-svg-nx5yTe9Ib5mdLmkW .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW span{fill:#333;color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node rect,#mermaid-svg-nx5yTe9Ib5mdLmkW .node circle,#mermaid-svg-nx5yTe9Ib5mdLmkW .node ellipse,#mermaid-svg-nx5yTe9Ib5mdLmkW .node polygon,#mermaid-svg-nx5yTe9Ib5mdLmkW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .rough-node .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label{text-anchor:middle;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .rough-node .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label{text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node.clickable{cursor:pointer;}#mermaid-svg-nx5yTe9Ib5mdLmkW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .arrowheadPath{fill:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster text{fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster span{color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nx5yTe9Ib5mdLmkW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW rect.text{fill:none;stroke-width:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape p,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label rect,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nx5yTe9Ib5mdLmkW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Host DI 容器
构造函数注入
构造函数注入
构造函数注入
构造函数注入
IEnumerable注入
IEnumerable注入
构造函数注入
构造函数注入
构造函数注入
IUfsAdapter → VendorUfsAdapter

(Singleton)
Form1

(Transient)
MainViewModel

(Transient)
TestController

(Singleton)
TestRunner

(Transient)
UfsService

(Transient)
ITestCase → ReadWriteTest

(Transient)
ITestCase → StressTest

(Transient)

从这张图里你能很容易看出,后面如果要扩测试项、替换 Adapter、调整 ViewModel 和流程层,改动入口其实已经预留好了。

其次是 Session 生命周期图:
#mermaid-svg-n3aSaD1qqX35kart{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n3aSaD1qqX35kart .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n3aSaD1qqX35kart .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n3aSaD1qqX35kart .error-icon{fill:#552222;}#mermaid-svg-n3aSaD1qqX35kart .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n3aSaD1qqX35kart .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .marker.cross{stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n3aSaD1qqX35kart p{margin:0;}#mermaid-svg-n3aSaD1qqX35kart defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-n3aSaD1qqX35kart .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-n3aSaD1qqX35kart .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-n3aSaD1qqX35kart .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-n3aSaD1qqX35kart .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-n3aSaD1qqX35kart .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel .label text{fill:#333;}#mermaid-svg-n3aSaD1qqX35kart .label div .edgeLabel{color:#333;}#mermaid-svg-n3aSaD1qqX35kart .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-n3aSaD1qqX35kart .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-n3aSaD1qqX35kart .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-n3aSaD1qqX35kart .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart #statediagram-barbEnd{fill:#333333;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .cluster-label,#mermaid-svg-n3aSaD1qqX35kart .nodeLabel{color:#131300;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-n3aSaD1qqX35kart .note-edge{stroke-dasharray:5;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note text{fill:black;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note .nodeLabel{color:black;}#mermaid-svg-n3aSaD1qqX35kart .statediagram .edgeLabel{color:red;}#mermaid-svg-n3aSaD1qqX35kart #dependencyStart,#mermaid-svg-n3aSaD1qqX35kart #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-n3aSaD1qqX35kart .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n3aSaD1qqX35kart :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 程序启动
TestRunner.RunAllAsync()

调用 OpenSession()
逐个执行 TestCase
执行下一个 TestCase
全部 TestCase 执行完毕

或发生异常

调用 CloseSession()
用户点击取消
finally 块中

调用 CloseSession()
资源释放完毕
未初始化
已初始化
运行中
已关闭
已取消

这张图对应的是整个工具后续最重要的一条原则:设备生命周期一定要收口管理,不要再散回每个测试项里。


二、第一步:把 VendorUfsAdapter 从占位类变成真实接入层

当前 VendorUfsAdapter 里的逻辑还是示意性的:

  • Initialize() 直接返回成功
  • Read() 返回固定大小的 byte 数组
  • Write() 默认返回 true
  • GetDeviceInfo() 返回一个固定字符串

这是非常合理的第一阶段做法,因为它能先把流程跑通。

但要进入真实工程阶段,Adapter 层至少要补 4 件事。

1)明确 SDK 封装边界

如果厂商提供的是 DLL,通常建议不要把 P/Invoke 直接散落在 VendorUfsAdapter 业务逻辑里。

更推荐拆成两层:

text 复制代码
VendorUfsAdapter
    -> VendorSdkClient
        -> DLL / IOCTL / Native API

这样做的好处是:

  • VendorUfsAdapter 仍然是面向业务语义的
  • 真正的 native 调用集中在更底层
  • 后期排查问题时,更容易区分"SDK 调用失败"还是"业务流程判断有误"

2)统一错误码与异常模型

真实硬件接入后,很少会只有一个 true/false

你大概率会遇到:

  • 超时
  • 设备未就绪
  • 读写失败
  • 参数非法
  • 权限不足
  • 驱动未安装
  • 设备拔出

如果这些情况直接原样抛给上层,UI 和测试项会很快被错误分支淹没。

更稳的方式是:

  • Adapter 层识别底层错误码
  • 转换成更一致的异常或结果对象
  • Service / TestCase 层只处理统一语义

例如:

  • UfsDeviceNotReadyException
  • UfsIoTimeoutException
  • UfsWriteFailedException

这样日志、提示、统计都会更清晰。

3)补全设备信息与能力探测

GetDeviceInfo() 不应该只返回一个字符串。

在真实项目中,通常值得扩展出更多信息,比如:

  • 厂商名
  • 型号
  • 容量
  • 固件版本
  • LUN 数量
  • Sector Size
  • 是否支持某些特性

如果不想一开始就大改接口,也可以先新增一个信息对象:

csharp 复制代码
public class UfsDeviceInfo

后面界面显示、报告输出、兼容性判断都会用得到。

4)把资源释放做扎实

占位阶段的 Dispose() 很简单,但真接 SDK 后一定要认真处理:

  • 句柄关闭
  • 缓冲区释放
  • 通讯上下文销毁
  • 重复释放保护

底层释放不彻底,是测试工具里非常常见但又很难查的一类问题。


三、第二步:让测试项从"能跑"升级成"可配置"

现在的测试用例是硬编码的,例如:

  • ReadWriteTest 默认写 LBA 0
  • StressTest 默认循环 1000
  • 数据块大小固定为 512

这在骨架阶段没问题,但工程里会很快不够用。

因为不同测试场景下,你需要调的参数非常多。

常见可配置项包括:

  • 起始 LBA
  • Sector Count
  • 循环次数
  • 随机种子
  • 写入模式(固定值 / 递增 / 随机)
  • 校验方式(全量比对 / CRC / 抽样)
  • 每轮间隔时间
  • 超时阈值

我建议的演进方式

不要一上来把所有参数都塞进 RunAsync(...)

更推荐两种方式之一:

方式 1:引入统一配置对象

例如:

csharp 复制代码
public class TestExecutionOptions
{
    public int StartLba { get; set; }
    public int SectorCount { get; set; }
    public int LoopCount { get; set; }
}

然后由 TestControllerTestRunner 在执行前把参数传给测试项。

方式 2:每个测试项有独立参数对象

例如:

  • ReadWriteTestOptions
  • StressTestOptions
  • PerformanceTestOptions

这种方式更适合测试种类差异很大的项目。

如果你的工具后面会做成正式 GUI,我更推荐第二种,因为它更利于和界面配置项一一对应。


四、第三步:给测试结果加更多"工程信息"

当前的 TestResult 已经有:

  • TestName
  • Passed
  • ErrorMessage
  • Duration
  • Timestamp

这作为最小闭环已经够了。

但真到团队协作阶段,这些信息通常还不够支撑复盘。

我建议补充这些字段:

  • Category:测试类别,例如功能、压力、性能
  • StepLogs:关键步骤日志摘要
  • StartLba / EndLba:本次操作范围
  • BytesTransferred:读写数据量
  • RetryCount:重试次数
  • DeviceInfoSnapshot:执行时设备快照
  • ErrorCode:底层错误码
  • ExtraData:扩展信息字典

这样做的价值是:

  • 结果更适合导出成报告
  • 出问题时更容易定位上下文
  • 不同测试项之间的横向对比也更自然

尤其是压力测试和性能测试,没有上下文信息的"成功/失败"其实意义有限。


五、第四步:把执行器从"串行跑完"升级成"可编排"

现在 TestRunner 的逻辑比较纯粹:

  • 打开 Session
  • 依次遍历 ITestCase
  • 收集结果
  • finally 关闭 Session

这已经很好了,但如果你要继续做强,后面可以考虑引入"编排能力"。

可演进方向 1:支持测试分组

例如:

  • 基础功能组
  • 稳定性组
  • 性能组
  • 工厂产测组

这样用户可以按场景选择,而不是每次都跑全量。

可演进方向 2:支持前置/后置动作

比如某些测试开始前需要:

  • 清空指定区域
  • 检查设备状态
  • 做一次重新初始化

测试结束后还可能需要:

  • 导出日志
  • 保存原始数据
  • 恢复设备配置

这些都可以逐步沉淀为 Runner 的可扩展管线。

可演进方向 3:支持失败策略

不同团队对失败后的处理策略不一样:

  • 遇到失败立即停止
  • 单项失败但继续跑后续测试
  • 自动重试若干次
  • 失败后自动采集更多诊断信息

这些策略如果提前留好接口,工具会更实用。


六、第五步:让取消、进度、状态反馈更完整

你现在已经通过 CancellationToken 打通了取消链路,这一点非常好。

下一步最值得补的是"进度可视化"。

因为真实长时测试里,用户最怕的不是慢,而是不知道它在干什么。

当前可以继续增强的点:

  • 总体进度:当前第几个测试 / 共几个测试
  • 单项进度:例如压力测试已经执行到第几轮
  • 当前动作:正在初始化、正在写入、正在校验、正在收尾
  • 预计剩余时间:尤其对长稳测试很有帮助
  • 取消中状态:点了取消后告诉用户系统正在回收资源

技术上怎么做?

比较自然的方式有两种:

1)事件通知

TestRunner / ITestCase 向外抛进度事件。

2)进度接口

例如引入:

csharp 复制代码
IProgress<TestProgressInfo>

这样 UI、日志系统、导出器都可以订阅同一份进度信息。

如果后面要做更丰富的界面,这一步非常值。


七、第六步:结果别只显示在 ListBox 里,要能沉淀成资产

现在 Form1 里已经可以把结果展示到列表中,这对演示和基础使用足够了。

但如果要进入真实项目场景,结果一定要能"带走"。

推荐的结果输出方向

1)文本日志

最低成本,便于快速追查。

2)CSV / Excel

适合测试人员横向比对、筛选、统计。

3)JSON

适合自动化系统二次消费。

4)HTML 报告

适合直接给团队、领导、客户看,阅读门槛低。

为什么结果持久化很重要?

因为测试工具的价值,不只是"当下跑完告诉你对不对",更在于:

  • 能不能追溯
  • 能不能复盘
  • 能不能横向比较不同设备/版本
  • 能不能沉淀成回归数据

很多时候,一份报告比一次弹窗更有价值。


八、第七步:界面层建议别急着复杂化,但要提前预留位置

工具开发特别容易陷入一个误区:

  • 一开始想把界面做得特别完整
  • 结果真正核心的硬件与测试流程反而推进很慢

所以我反而建议:前期 UI 保持克制,但预留扩展位。

这个项目接下来适合增加的 UI 区域

  • 设备信息区:显示型号、容量、固件版本
  • 测试项选择区:勾选要执行的用例
  • 参数配置区:循环次数、LBA、块大小等
  • 运行状态区:当前测试、进度、耗时
  • 日志区:滚动显示实时日志
  • 结果区:列表 + 详情面板

这样用户既能快速上手,也能承载越来越复杂的测试能力。


九、如果是我继续迭代,我会按什么优先级做?

如果你现在要把这套骨架继续往前推,我建议按下面顺序迭代:

第一优先级:接真实设备

先把 VendorUfsAdapter 打通。

因为没有真实硬件链路,很多上层能力的价值都还只是"逻辑正确"。

第二优先级:参数配置化

让测试不再依赖硬编码,能针对不同场景调整。

第三优先级:结果与日志落盘

保证每次执行都有迹可循。

第四优先级:进度与状态反馈

提升长时间测试的可用性。

第五优先级:测试编排与分组

让工具更接近正式的测试平台形态。

这个顺序的本质是:先打通真实能力,再提升可用性,最后增强平台化能力。


十、这套骨架适合哪些场景?

虽然项目名是 UFS,但我觉得这套结构并不只适合 UFS。

它同样适用于:

  • eMMC 测试工具
  • NVMe 测试工具
  • 串口/USB/PCIe 设备验证工具
  • 工厂产测工具
  • 内部实验室自动化验证工具

因为它抓住的是一类通用问题:

  • 底层设备接口变化大
  • 测试项数量会持续增加
  • 需要 UI 承载与结果展示
  • 需要统一调度、取消、日志、报告

所以你可以把它理解为:

一个面向硬件测试工具的轻量工程骨架。


十一、结语

我一直觉得,测试工具真正的难点,不在于"写出第一个功能",而在于:

  • 两个月后能不能继续加新测试
  • 半年后能不能换设备实现
  • 一年后能不能追溯某次失败现场

从这个角度看,一个好的骨架,比仓促堆出来的功能更重要。

当前这个 UFS 测试工具虽然还处在早期阶段,但它已经把后续最重要的扩展路径留出来了:

  • 可以接真实 SDK
  • 可以扩更多测试项
  • 可以沉淀日志与报告
  • 可以逐步走向平台化