4. 参数化设计:使用argparse解析命令行参数,让脚本更灵活

说实话,我见过太多测试脚本写死在代码里了。

比如测试地址写死、端口写死、并发数写死。每次换个环境,就得改代码。改完还得提心吊胆,生怕改漏了哪里。我自己刚入行那会儿也干过这种事,后来被坑过一次——上线前忘了把测试地址改回生产地址,结果跑了半天假数据。嗯,从那以后,我就养成了一个习惯:所有可能变的东西,都交给命令行参数。

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
我的习惯: 每个参数都给一个合理的默认值。这样80%的场景下,用户只需要传必填参数就能跑起来。默认值选最常用的那个,比如并发数默认1(安全),超时默认300秒(够用)。

4.5 避坑指南

我曾经踩过的坑:
  • 参数名冲突: 有一次我用了-c作为简写,结果和系统命令冲突了。后来我养成了习惯,简写只用大写字母或者不常用的字母。
  • 类型忘记指定: 如果不加type=int,传进来的数字其实是字符串。做比较的时候'10' < '5'会返回True,因为字符串比较是按字典序。这个bug我找了一下午。
  • 路径处理: 用户传的相对路径,在脚本里要转成绝对路径。不然换个目录执行脚本,路径就失效了。

4.6 知识体系总览

下面这张图,把参数化设计的核心逻辑串起来了:

参数化设计核心逻辑 命令行参数 argparse 解析 args 对象 必填参数 --cases, --input 可选参数 --concurrency, --timeout 开关参数 --verbose, --debug 灵活、可复用的测试脚本 不改代码,适应所有场景

说白了,参数化设计就是把「变」和「不变」分开。不变的逻辑写在代码里,变的东西交给参数。这样你的脚本才能从「一次性工具」进化成「可复用的测试框架」。

我个人觉得,学会argparse是测试脚本进阶的第一步。它不复杂,但带来的灵活性是质的飞跃。下次写脚本的时候,不妨多问自己一句:这个值以后会不会变?如果会,就做成参数。