4. 参数化设计
说实话,我见过太多测试脚本写死在代码里了。
比如测试地址写死、端口写死、并发数写死。每次换个环境,就得改代码。改完还得提心吊胆,生怕改漏了哪里。我自己刚入行那会儿也干过这种事,后来被坑过一次——上线前忘了把测试地址改回生产地址,结果跑了半天假数据。嗯,从那以后,我就养成了一个习惯:所有可能变的东西,都交给命令行参数。
4.1 为什么需要参数化?
你想想看,一个仿真脚本可能要面对这些场景:
- 今天跑测试A,明天跑测试B
- 本地调试用10个用例,CI环境要跑1000个
- 有时候需要输出详细日志,有时候只要结果
如果这些全靠改代码实现,那测试工程师就变成「改代码工程师」了。参数化的核心思想很简单:把脚本的行为和脚本的代码解耦。代码只负责逻辑,行为由参数决定。
4.2 argparse 基础用法
Python自带的argparse模块,就是干这个的。我个人觉得它比sys.argv好用太多了——自动生成帮助信息、自动类型转换、自动报错提示,省心得很。
先看一个最简单的例子:
import argparse
def main():
parser = argparse.ArgumentParser(description='批量运行测试用例')
parser.add_argument('--test-file', required=True, help='测试用例文件路径')
parser.add_argument('--concurrency', type=int, default=5, help='并发数,默认5')
args = parser.parse_args()
print(f'测试文件: {args.test_file}')
print(f'并发数: {args.concurrency}')
if __name__ == '__main__':
main()
运行起来是这样的:
python run_tests.py --test-file cases.csv --concurrency 10
# 输出:
# 测试文件: cases.csv
# 并发数: 10
如果用户忘了传必填参数,argparse会自动报错并提示用法。这个设计我觉得很贴心——与其让脚本在运行到一半时崩溃,不如一开始就拦住错误。
4.3 常用参数类型与技巧
在实际项目中,我常用的参数类型有这么几种:
| 参数类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | --env prod |
环境名称,最常用 |
| 整数 | --count 100 |
用例数量、并发数等 |
| 布尔开关 | --verbose |
加了这个参数就开启详细日志 |
| 列表 | --tags smoke regression |
按标签筛选用例 |
| 文件路径 | --output ./result.json |
输出结果保存位置 |
这里有个小技巧:布尔开关用action='store_true'。比如:
parser.add_argument('--verbose', action='store_true', help='输出详细日志')
# 使用时:python script.py --verbose
# args.verbose 就是 True,否则是 False
我曾经在项目里见过有人用字符串传布尔值,比如--verbose true,然后在代码里判断if args.verbose == 'true'。嗯,这也能跑,但不够优雅。用store_true才是正道。
4.4 实战:给仿真脚本加上参数
结合我们之前的批量运行脚本,来看看参数化之后长什么样:
import argparse
import json
import time
def parse_args():
parser = argparse.ArgumentParser(description='仿真脚本批量运行测试用例')
# 必填参数
parser.add_argument('--cases', required=True,
help='测试用例文件(JSON格式)')
# 可选参数
parser.add_argument('--concurrency', type=int, default=1,
help='并发执行数,默认1(串行)')
parser.add_argument('--timeout', type=int, default=300,
help='每个用例超时时间(秒),默认300')
parser.add_argument('--output', default='./result.json',
help='结果输出路径,默认./result.json')
parser.add_argument('--verbose', action='store_true',
help='输出详细执行日志')
parser.add_argument('--retry', type=int, default=0,
help='失败重试次数,默认0不重试')
return parser.parse_args()
def run_tests(args):
# 加载用例
with open(args.cases, 'r') as f:
cases = json.load(f)
if args.verbose:
print(f'加载了 {len(cases)} 个测试用例')
print(f'并发数: {args.concurrency}')
print(f'超时时间: {args.timeout}s')
# 执行逻辑(简化版)
results = []
for case in cases:
start = time.time()
try:
# 模拟执行
time.sleep(0.1)
results.append({
'name': case['name'],
'status': 'pass',
'duration': time.time() - start
})
if args.verbose:
print(f' ✓ {case["name"]} 通过')
except Exception as e:
results.append({
'name': case['name'],
'status': 'fail',
'error': str(e)
})
if args.verbose:
print(f' ✗ {case["name"]} 失败: {e}')
# 保存结果
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
if args.verbose:
print(f'\n结果已保存到 {args.output}')
if __name__ == '__main__':
args = parse_args()
run_tests(args)
现在,你可以这样用:
# 本地调试,只跑1个用例,看详细日志
python run_tests.py --cases test_smoke.json --verbose
# CI环境,并发跑10个,超时设长一点
python run_tests.py --cases test_full.json --concurrency 10 --timeout 600
# 生产环境,不输出日志,结果保存到指定位置
python run_tests.py --cases test_regression.json --output /data/reports/result.json
4.5 避坑指南
- 参数名冲突: 有一次我用了
-c作为简写,结果和系统命令冲突了。后来我养成了习惯,简写只用大写字母或者不常用的字母。 - 类型忘记指定: 如果不加
type=int,传进来的数字其实是字符串。做比较的时候'10' < '5'会返回True,因为字符串比较是按字典序。这个bug我找了一下午。 - 路径处理: 用户传的相对路径,在脚本里要转成绝对路径。不然换个目录执行脚本,路径就失效了。
4.6 知识体系总览
下面这张图,把参数化设计的核心逻辑串起来了:
说白了,参数化设计就是把「变」和「不变」分开。不变的逻辑写在代码里,变的东西交给参数。这样你的脚本才能从「一次性工具」进化成「可复用的测试框架」。
我个人觉得,学会argparse是测试脚本进阶的第一步。它不复杂,但带来的灵活性是质的飞跃。下次写脚本的时候,不妨多问自己一句:这个值以后会不会变?如果会,就做成参数。