用Excel驱动C#自动化测试平台:告别写死用例,轻松维护回归测试
2026/9/9 20:04:15 网站建设 项目流程

如果你在维护一个需要反复回归的测试项目,应该能体会到那种痛:用例一多,断言写死在代码里,功能稍微调整一下就要重新编译、重新部署,测试人员想改一条数据还得找你排队。我前段时间正好把一套用C#写的自动化测试平台完整重构了一遍,核心思路就是让测试用例不再“锁在代码里”,而是全部通过Excel测试序列配置来驱动。简单说,测试人员打开Excel,把用例ID、操作步骤、参数、预期结果按列填好,保存后平台就能自动读取并执行整个测试流程,执行完输出报告。整个过程不用改一行代码,也不用重新编译。

这篇文章会把平台的方案选型、架构设计、Excel模板规范、核心代码实现以及我实际踩过的坑完整拆开讲。适合正在做接口自动化、UI自动化,想降低用例维护成本,或者打算自己搭一套轻量级自动化测试平台的团队和个人参考。

1. 项目背景与方案选型

1.1 为什么选择C#开发自动化测试平台

自动化测试平台的语言选型,其实没有绝对答案。Python生态有pytest、robotframework,Java有TestNG、JUnit,但C#在这个场景里被低估了。

我选C#主要看重几点。第一是Windows环境下的企业级成熟度,大多数做自动化测试的团队跑在Windows服务器或PC上,C#的WinForms/WPF做管理界面非常顺手,不需要额外搭前端工程;第二是如果需要做Windows桌面应用的UI自动化,C#生态里有FlaUI、TestStack.White这些库,可以直接操作Win32控件,这是Python和Java很难替代的优势;第三是C#的强类型和异步模型,写测试执行引擎时类型安全能减少大量低级错误,async/await处理超时、并发、等待也比回调嵌套清爽很多。

如果你是做接口自动化为主,C#的HttpClient、System.Text.Json等都是内置能力,不需要引第三方库就能把请求、响应、断言串起来。如果是做Web UI自动化,也能用Selenium WebDriver的C#绑定。所以C#不是只能写上位机和企业系统,它做测试平台底座是够扎实的。

1.2 为什么用Excel承载测试序列配置

市面上很多框架用YAML、JSON存用例,或者直接在代码里写测试方法。这两种方式我最初都试过,最后都放弃了,原因很现实:团队里真正写用例的人不是纯开发,他们更熟悉Excel。

Excel做测试序列配置的优势是碾压级的。第一,学习成本几乎为零,测试人员日常就在用Excel,下拉框、数据验证、条件格式都能用来提升录入体验;第二,Excel天然支持批量操作,一次粘贴几十行测试数据进去很轻松,换成YAML得手敲缩进;第三,改完保存就能生效,平台定时或手动重新加载文件,不需要重启程序;第四,Excel是通用格式,产品、开发、测试都能打开看,跨岗位沟通成本低。

当然Excel也有缺点,比如二进制格式在Git里diff不友好,多人同时编辑容易冲突。这个我后面在实战中会用“按模块拆分文件”的方式缓解。整体来说,Excel作为测试人员日常接触最多的工具,用来做序列配置,落地阻力最小。

2. 平台架构与核心模块设计

2.1 整体架构拆解

平台整体分三层:数据层、执行层、展示层。

数据层负责把Excel内容读进内存,转换成一个个TestCase和TestStep对象。这里最关键的是用NPOI而不是Excel COM组件,因为NPOI不依赖目标机器安装Office,服务器上没有Excel也能跑,也不会有Office弹窗把执行卡住的问题。执行层是核心引擎,它拿到数据层转换好的对象列表后,按照步骤顺序逐条执行,每条操作都是一个可扩展的Action,通过字符串关键字映射到具体的C#方法。展示层由WinForms界面、控制台日志和HTML报告组成,WinForms负责可视化加载配置和启动执行,HTML报告给管理层和测试人员看执行结果。

三层之间用接口隔离,数据层不关心执行逻辑,执行层也不关心数据来源是Excel还是数据库。这个解耦设计很重要,后面如果想把用例迁移到数据库,只需要重写一个IDataSource实现,执行引擎完全不用动。

2.2 核心数据结构:TestCase和TestStep

测试序列落到代码里,本质上就是两条数据结构。我没有把它们设计得很复杂,够用就好。

public class TestStep { public string StepId { get; set; } public string StepName { get; set; } public string Action { get; set; } public string Parameter { get; set; } public string ExpectedType { get; set; } public string ExpectedValue { get; set; } public int Timeout { get; set; } public bool IsEnabled { get; set; } } public class TestCase { public string CaseId { get; set; } public string CaseName { get; set; } public string Module { get; set; } public List<TestStep> Steps { get; set; } = new(); }

Action是操作关键字,比如OpenUrl、InputText、Click、SendRequest、AssertEqual这些。Parameter是操作参数,我统一用JSON字符串承载,好处是不需要为每个Action单独定义Excel列。一段JSON比拆成五六个单元格更直观,也方便表达嵌套结构,比如登录请求要传username和password两个字段,用{"username":"admin","password":"123456"}一行就写完了。

ExpectedType是断言类型,ExpectedValue是预期值。实际执行时,Action执行完会产出一个实际结果,然后交给断言处理器去比对。Timeout控制单步超时,IsEnabled用来快速跳过某些步骤。测试人员做回归时,经常需要临时禁用一个用例,直接在Excel里把IsEnabled改成FALSE,保存重跑即可。

2.3 执行引擎:从关键字到方法的映射

执行引擎是整个平台的心脏,它要解决的核心问题是怎么把Excel里的Action字符串变成真正执行的C#方法。我采用的方案是用字典做映射,加载完Excel后,把每一行的Action作为key,找到对应的处理委托,然后调用。

private readonly Dictionary<string, Func<TestStep, TestContext, Task>> _actions = new(); public void RegisterAction(string actionName, Func<TestStep, TestContext, Task> handler) { _actions[actionName] = handler; } private async Task ExecuteStepAsync(TestStep step, TestContext context) { if (_actions.TryGetValue(step.Action, out var handler)) { await handler(step, context); } else { throw new NotSupportedException($"发现未注册的操作关键字: {step.Action}"); } }

这种设计的好处是扩展成本极低。要加一个新操作,只需要写一个方法,然后在启动时调用RegisterAction注册进去就行。想加“滑动验证码”“上传文件”这类特殊操作,完全不用改引擎核心,只增加业务层的Action方法即可。

TestContext在这里承担了变量传递和状态共享的功能。比如登录步骤执行完,把登录返回的token写进Context.Variables["token"],后续步骤的Parameter里写${token},执行前统一做变量替换。这样测试人员就可以在Excel里实现数据依赖,不用写代码。

2.4 辅助模块:报告与日志

执行完一轮测试,如果只输出一个“通过/失败”的状态,测试人员根本没法排查问题。我做了三层输出。

第一层是控制台实时日志,执行每一步时打印当前步骤名、执行结果、耗时,方便现场盯着跑。第二层是文件日志,用Serilog写入按天滚动的log文件,内容包含异常堆栈、请求响应原文,排查故障时翻日志最管用。第三层是HTML报告,执行结束后汇总生成,按模块分组展示用例通过率、失败步骤的具体操作和预期/实际值。如果在UI自动化里还做了截图,报告页面会直接展示失败截图,这个对定位问题非常直观。

报告命名我习惯用时间戳加执行批次号,保存在Reports目录下,默认只保留最近30份,避免磁盘被撑爆。

3. Excel测试序列配置的实操细节

3.1 Excel模板设计规范

Excel能不能被程序稳定解析,完全取决于模板规不规范。我在模板里强制规定了几条规则。

表格列结构固定为:CaseId、CaseName、Module、StepId、StepName、Action、Parameter、ExpectedType、ExpectedValue、Timeout、IsEnabled。第一行必须是表头,程序从第二行开始读。同一个用例的多条步骤,CaseId相同,StepId递增,这样读取后可以用LINQ先按CaseId分组,再按StepId排序。

每行只允许是一条步骤。不要在Excel里合并单元格,合并后的单元格在NPOI中只有左上角的cell有值,其余区域读出来是null,我之前在这里吃了大亏,后面会细说。

我会额外建一个“参数配置”Sheet,存放全局变量,比如测试环境地址BaseUrl、数据库连接串、超时配置。执行引擎加载用例前先读取这个Sheet,把变量注入TestContext,用例里只用${BaseUrl}引用,环境切换时只需要改配置Sheet,不需要改用例。

下面是一个简化版的模板示例:

CaseIdCaseNameModuleStepIdActionParameterExpectedTypeExpectedValue
TC001登录成功用户中心1SendRequest{"method":"POST","path":"/api/login","body":{"username":"admin","password":"123456"}}Equal{"code":200}
TC001登录成功用户中心2AssertField{"path":"data.token","notEmpty":true}True
TC002获取用户信息失败用户中心1SendRequest{"method":"GET","path":"/api/user/999","headers":{"token":"${token}"}}Equal{"code":404}

实际项目里Parameter会比这个复杂,但结构就是这样。这样做的好处是所有用例都“可见”,产品经理也能看懂在测什么。

3.2 NPOI读取Excel的正确姿势

NPOI读取Excel文件本身不复杂,但有很多隐藏坑。核心思路是先打开工作簿,读取数据Sheet,然后逐行逐列解析。

using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public List<TestCase> LoadFromExcel(string filePath) { var testCases = new List<TestCase>(); using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); IWorkbook workbook = new XSSFWorkbook(fileStream); var sheet = workbook.GetSheet("用例"); if (sheet == null) throw new Exception("未找到名为“用例”的Sheet"); for (int rowIndex = 1; rowIndex <= sheet.LastRowNum; rowIndex++) { var row = sheet.GetRow(rowIndex); if (IsBlankRow(row)) continue; var caseId = GetCellValue(row.GetCell(0)); if (string.IsNullOrWhiteSpace(caseId)) continue; // 解析各列,构建TestStep,再按CaseId分组为TestCase } return testCases; }

这里有个重要细节:文件流要加FileShare.ReadWrite。因为测试人员可能在执行间隙用Excel打开文件查看,如果不加共享权限,程序会报“文件被占用”直接崩溃。加了ReadWrite后,即使Excel打开着也能读到内容,虽然不推荐执行期间去改文件,但至少不会因为误操作挂掉。

GetCellValue方法必须处理所有单元格类型。Excel单元格有String、Numeric、Boolean、Formula、Blank五种常见类型,只调用ToString()会拿不到数字和日期的正确格式。NPOI读取日期单元格拿到的是Excel序列号,必须先判断DateUtil.IsCellDateFormatted(cell),再转成DateTime。公式单元格要读CachedFormulaResultType,才能拿到Excel最后一次计算好的结果。

3.3 测试序列的组织与执行顺序

读取完成后,内存里是一长串TestStep。执行前要先做两步加工:按CaseId分组,组内按StepId排序。分组是为了保证单个用例的步骤连续执行;排序是为了防止Excel行顺序错乱导致步骤颠倒。

执行顺序上,我采用的策略是:先执行所有IsEnabled=True的用例,IsEnabled=False的用例直接跳过并统计为“跳过”。单个用例内,如果某一步失败,默认中止当前用例,标记为Failed,然后继续执行下一条用例。这样做的好处是,一条用例挂掉不会拖垮整个回归批次,测试人员最后看报告时能一眼看到哪些模块挂了,而不是等全部跑完才看到第一处失败。

如果要支持“失败后重试”,我额外加了一个RetryTimes列,执行引擎发现步骤失败后会重新执行,超过重试次数才标记失败。这个功能对处理偶发性的网络抖动非常有用,但注意不要设置过大的重试次数,否则整体执行时间会翻好几倍。

注意:Excel模板里尽量不要使用跨行合并单元格。NPOI读取合并区域时,只有区域左上角的Cell有值,其他单元格返回null,这会导致步骤行丢失。如果确实要用合并单元格展示分组信息,建议只在模板的展示Sheet里用,解析时用独立的数据Sheet。

4. 关键功能的实现细节

4.1 数据驱动与变量替换机制

自动化测试绕不开数据驱动。同一个登录接口,要测正常密码、错误密码、空密码、锁定账号这四组数据,最笨的办法是复制四条用例,改成不同参数。我用的是变量替换加外部数据源的方式。

参数模板里的${变量名}在每次执行前都会被替换成TestContext里存储的实际值。这个替换逻辑我用Regex实现,简单高效。

private static readonly Regex VariablePattern = new Regex(@"\$\{([^}]+)\}", RegexOptions.Compiled); public static string ReplaceVariables(string template, TestContext context) { if (string.IsNullOrEmpty(template)) return template; return VariablePattern.Replace(template, match => { var key = match.Groups[1].Value; return context.Variables.TryGetValue(key, out var value) ? value : match.Value; }); }

变量来源有三个。一是全局配置Sheet,比如BaseUrl、AppId这类环境级变量,加载时直接写入Context。二是步骤执行过程中产生的数据,比如登录返回的token、创建订单返回的订单号,通过SetVariable这个Action写入Context。三是外部数据文件,我支持在Excel里指定DataFile列,内容指向一个CSV或另一个Excel文件,执行时会遍历数据文件里的每一行,用当前行数据替换模板变量,实现一组参数跑多次的效果。这个机制对接口测试特别有用,几十组参数只用维护一条模板用例。

需要特别提醒的是,变量替换后最好先打印一遍替换结果到日志,方便查“变量没替换成功”的问题。我遇到过不少次是因为拼写不一致,Excel里写的是${Token},代码里存的是token,大小写不匹配导致替换失败。

4.2 断言机制与结果判定

断言是测试平台里最容易写糙的部分。我最初的版本每个Action内部都自带断言逻辑,结果就是断言散落在各处,想统一加日志都无从下手。后来我抽了一个IAssertHandler接口,让每种断言类型单独实现,统一调度。

public interface IAssertHandler { bool Assert(string actual, string expected); } public class EqualAssertHandler : IAssertHandler { public bool Assert(string actual, string expected) { return string.Equals(actual?.Trim(), expected?.Trim(), StringComparison.OrdinalIgnoreCase); } } public class ContainsAssertHandler : IAssertHandler { public bool Assert(string actual, string expected) { return actual != null && actual.Contains(expected); } }

断言处理器注册表也是字典,ExpectedType列填的是Equal还是Contains,引擎就从字典里取出对应的处理器。目前我内置了Equal、NotEqual、Contains、NotContains、RegexMatch、GreaterThan、LessThan七种,覆盖了绝大多数接口和UI测试场景。如果是数值比较,输入参数会被转换成double后再比较,转换失败时直接断言失败,而不是静默把字符串当数字用,这个坑一定不要踩。

断言结果必须写入执行结果对象,包含实际值、期望值、断言类型、时间戳。报告页面上要能直接看到“实际值:xxx 期望值:xxx”的对比,不能只给一个红叉,否则定位问题还是要翻日志。

4.3 HTML报告生成与历史追溯

报告我用最简单的方式生成:执行结束后,把所有结果序列化成对象列表,塞进一个Razor模板或者StringBuilder拼HTML。不需要上重型前端框架,一个带样式表格的HTML足够清晰。

报告顶部是统计摘要:总用例数、通过、失败、跳过、通过率、总耗时。中间是按模块拆分的用例明细表,展开后能看到每个步骤的操作动作、参数、预期结果、实际结果、耗时和状态。底部是失败用例的详细日志,直接展示异常消息和堆栈,方便开发快速定位。

文件名格式我用report_yyyyMMdd_HHmmss.html,执行批次的上下文信息也写进HTML,比如执行机器名、测试环境、Excel文件路径。历史追溯很重要,有一次测试人员反馈“昨天还通过今天失败了”,我通过报告里的环境参数发现他切换了测试环境但忘了改配置,查起来很清楚。

5. 常见问题与排查技巧实录

5.1 Excel读取相关的坑

NPOI读取Excel的问题,我遇到最多的是公式单元格没缓存导致读取结果为空。用Excel COM打开过的文件会自动缓存公式结果,但如果这个文件是程序批量生成的,公式单元格可能只有公式没有结果值,这时读CachedFormulaResultType会得到CellType.Unknown或空字符串。

解决方案是用NPOI的FormulaEvaluator手动求值。还有单元格数字类型变成科学计数法的问题,比如读取长ID号时得到“1.23457E+17”,解决方案是统一用Decimal转换,或者在模板设置单元格格式为文本。第一种情况更普遍,建议读Numeric时不要直接ToString(),而是先转decimal再ToPlainString。

空行判断也需要单独写方法。Excel里执行过删除行操作后,LastRowNum可能比实际数据行大,最后几行全是null引用。我的IsBlankRow方法会检查整行所有单元格是否都为空,只要有一个非空就保留。这样能避免把末尾空行读成空步骤,也不漏掉真正有数据的行。

5.2 执行引擎相关的坑

异步问题是最隐蔽的。UI自动化里执行一个Click,如果用的是fire-and-forget方式,不等操作完成就继续下一步,那下一步可能操作的是旧页面状态,导致一系列连锁失败。所以执行引擎的ExecuteStepAsync必须严格await每一步,不能让子任务在后台游离。另外还要给HttpClient请求设置明确的Timeout,默认100秒的Timeout在网络环境差的时候会拖垮整个测试批次,我统一在Startup时设置请求超时为15秒。

步骤间共享状态丢失的问题也很常见。每个人的第一版引擎都可能把状态存在方法内的局部变量里,步骤执行完后变量就没了。正确的做法是把状态放TestContext里,整个用例执行期间都存在。我在测试脚本里专门写了用例:第一步写入变量,第二步读取并断言,确保Context贯穿始终。

5.3 高频问题速查表

现象可能原因解决方案
Excel步骤读取为空合并单元格或末尾空行设置数据Sheet禁用合并,使用IsBlankRow过滤空行
日期格式错乱读取到Excel序列号未转换用DateUtil.IsCellDateFormatted判断后转DateTime
公式单元格取值异常公式结果未缓存调用FormulaEvaluator手动求值
变量替换后还是${xxx}变量名拼写或大小写不一致打印替换结果排查,模板与代码保持统一命名
断言总是失败实际值包含空格或换行断言前统一Trim(注意区分有需要保留空格的场景)
执行到一半崩溃未注册的Action关键字加载Excel时校验所有Action,给出明确报错
Excel打开时程序读取失败文件被独占锁定FileStream加FileShare.ReadWrite
一条失败导致后续全部不跑用例隔离没做好用例级try-catch,单条失败不影响其他用例

这张表我贴在了平台帮助文档第一页,新接手的人遇到问题先查表,能解决80%的疑问。

6. 后续扩展与个人心得

这套平台跑起来后,我陆续给它加了几个扩展点,目前看都是值得投入的方向。第一是接入Jenkins,命令行传参指定Excel文件路径和环境,这样每晚定时回归就能自动执行、自动出报告。第二是支持FlaUI做Windows桌面端UI自动化,通过Action关键字把Win32控件操作也纳入同一套序列配置,这样接口层和UI层能共用一套平台,不用维护两套框架。第三是报告集成到Allure,虽然HTML报告够用,但Allure在历史趋势和用例管理上更专业,团队管理要求高的时候可以切换。

最后说点个人体会。做自动化测试平台,最核心的不是框架有多炫,而是能不能降低用维护成本。用Excel做测试序列配置最大的价值,是让非开发人员也能参与用例维护,这比任何技术选型都重要。我刚开始做的时候也追求过“大而全”,想把所有能力一次铺开,结果维护成本很高。后来调整思路,先把最频繁回归的核心流程用Excel序列写出来跑通,有了实际收益,团队自然愿意用。如果你也想搭这么一套平台,建议从小处入手,先把一个模块的用例跑起来,再逐步扩展,这条路我走过,踏实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询