我刚开始接触自动化测试那会儿,最怕的不是用例写不好,而是不知道Python该学到什么程度。网上搜Python核心语法,出来的都是动辄几百页的教程,从并发编程讲到元类,看得人头皮发麻。后来真正把测试脚本写起来才发现,测试脚本开发用的语法其实是个很窄的子集:会定义变量、存数据、写循环、拆函数、读文件、抓异常,再把类和对象的基本玩法搞明白,就已经能应付绝大多数自动化场景了。这篇文章就是按这个思路来的,用测试脚本的实际需要倒推Python核心语法,把每个语法点都放在真实的测试场景里讲清楚。适合准备入门自动化测试的QA、刚转型做测试开发的同学,也适合那些学过Python但总觉得和实际工作对不上号的人。读完之后,你至少能独立写出一条能跑的接口测试脚本,并且知道怎么把它拆成方便维护的小模块。
1. 测试脚本到底要学多少Python?先划掉不考的语法
1.1 测试脚本主要干什么活
从自动化测试的日常任务来看,脚本要干的活其实很固定:
- 准备测试数据:从配置、Excel、JSON里读取用户名、密码、URL等参数。
- 发送请求或执行操作:接口脚本用HTTP请求,UI脚本用元素定位和点击。
- 校验结果:把实际返回值和预期值做对比,输出通过或失败。
- 记录日志和报告:错误信息落盘、生成测试报告。
- 批量重复:对一组数据反复执行同一个用例,统计结果。
这些场景对应的Python语法,无非就是变量、数据类型、条件、循环、函数、文件读写、异常处理和类。看起来内容不多,但只要把这些吃透,日常测试脚本就没问题了。我见过不少同学一上来就研究Python的语法细节,比如列表推导式怎么写更优雅、字典合并有几种写法,这些确实有趣,但对测试脚本开发的初期帮助不大。
1.2 先划掉不需要着急学的部分
很多人学Python半途而废,原因是把范围铺得太大。站在测试脚本开发的角度,下面这些可以往后放:
| 先不用急着学 | 理由 |
|---|---|
| 装饰器 | 写框架时经常用,但入门阶段用不到,等需要给用例加日志、加耗时统计时再补 |
| 生成器和迭代器 | 处理超大日志时有用,普通测试数据量完全不需要 |
| 多线程和协程 | 并发压测才会用到,先把单条用例跑通再说 |
| 正则表达式 | 只有做复杂字符串提取才需要,入门可以先跳过 |
| 元类、描述符 | 属于语言进阶,测试脚本极少涉及 |
| 网络编程 | 有requests库,不需要自己处理socket |
明确这个范围之后,学习路线就清晰了:先把数据结构、流程控制、函数、文件、异常、类这六大块做扎实,再回到测试场景里反复用。学语法本身不是目的,能写出稳定运行的脚本才是目的。所以这篇文章后面每一个章节,我都会先讲语法,再给测试场景里的例子。
2. 环境准备最容易翻车的三个细节:版本、解释器、虚拟环境
2.1 版本选择与安装
新装环境我建议直接选Python 3.10或更新的稳定版。现在很多测试框架和第三方库都已经转向Python 3,老掉牙的Python 2早就不维护了,想学就直接学3。Windows安装时最关键的步骤是勾选左下角的Add Python to PATH,我见过太多人栽在这一步,装完在命令行敲python提示不是内部或外部命令,其实就是PATH没配上。装完在终端验证一下:python --version。
如果公司内部有老系统指定了Python 3.6或3.8,也别慌,语法上对测试脚本开发影响不大,个别新特性用不了而已。Linux系统如果自带Python,尽量别去动系统自带的版本,直接用python3命令调用,再把第三方库装到虚拟环境里,避免把系统环境搞得一团糟。很多网上的教程会教你怎么把系统Python换成新版本,作为测试工程师不建议这么做,系统组件依赖旧Python的情况比想象中多,真出了问题恢复成本很高。
2.2 解释器路径和pip的对应关系
在命令行执行Python脚本的时候,最容易迷惑的是"我明明装了库,运行时却说找不到"。绝大多数情况是命令对应的解释器不对:一个机器上装了多个Python,python指向3.8,pip却指向3.10,结果库装到了另一个版本里。保险的做法是不要直接用pip install,而是始终用python -m pip install,这样保证装进当前python解释器对应的环境。
还有个常见问题是CMD里输入python没反应,但开始菜单里的Python能打开。原因在于Windows应用商店或多个版本的环境变量顺序。排查思路很简单:在CMD里where python,看看解释器路径到底指向哪里。把这个问题搞清楚,后面所有库安装问题都能少一半。如果发现命令行里的python指向了Windows应用商店的占位程序,去系统设置里调整环境变量路径顺序就行。
2.3 虚拟环境为什么必须用
测试项目最好每个都建独立虚拟环境,共享系统环境短期省事,长期必然出问题。一个项目要pytest 6,另一个要pytest 8,放一起就会冲突。创建虚拟环境很简单:
python -m venv venv # Windows下激活 venv\Scripts\activate # Linux/Mac下激活 source venv/bin/activate激活后命令行前缀会多一个(venv),这时再python -m pip install requests pytest,库就只装在这个项目里。写测试脚本通常还会用到这几个库:
| 库 | 用途 |
|---|---|
| requests | 发HTTP请求,接口自动化核心 |
| pytest | 测试用例组织与断言框架 |
| pytest-html | 生成HTML测试报告 |
| pyyaml | 读取yaml配置文件 |
| openpyxl | 操作Excel测试数据 |
环境弄得利索,后面写脚本会非常省心。大多数入门者遇到"脚本在我电脑上跑不通",查到最后都是环境问题,而不是代码问题。所以我把环境这一章放在最前面,值得多花二十分钟一次配好。
3. 从变量到数据结构:测试数据怎么存才顺手
3.1 变量不用声明类型,但要小心动态类型
Python定义变量不需要写类型:
url = "https://api.example.com/login" port = 8080 is_https = True这让入门非常轻松,但也带来了一个坑:变量在运行时才确定类型。从配置文件或Excel里读出来的数字,往往是字符串"200"而不是整数200,直接拿去做比较就会失败。
code_from_response = "200" if code_from_response == 200: print("通过") # 不会执行,因为一个是字符串一个是整数所以测试脚本里最常用的操作之一就是类型转换。int("200")转整数,str(200)转字符串,float("3.14")转小数。写断言前先确认两边类型一致,这是排查"代码看起来对就是不通过"的第一思路。Python有个内置函数type()可以在调试时快速看变量类型,拿不准的时候print(type(变量))一下,比自己瞎猜强多了。
3.2 列表和字典是测试脚本的两大主角
列表用来存一组测试数据:
test_users = ["alice", "bob", "carol"] for user in test_users: print(user)字典用来表达一条完整的测试用例,最贴近接口请求和响应的结构:
case = { "name": "登录成功", "method": "POST", "url": "/api/login", "payload": {"username": "alice", "password": "123456"}, "expected": 200, }接口返回的JSON解析之后就是字典或嵌套的字典列表,所以字典的增删改查必须熟练。取value用case["name"],安全一点用case.get("name"),避免KeyError。嵌套结构虽然看起来复杂,其实就是一层层剥洋葱:data["payload"]["username"]。刚开始写测试数据时,宁可在字典里多写几个字段,也别图省事省略,因为后面一旦需要新增参数,结构化数据改起来最方便。
3.3 元组和集合的特定用途
元组和列表很像,但创建后不能修改,适合保存固定不变的参数,比如数据库连接信息。函数返回多个值的时候,本质上返回的也是一个元组。集合最大的用途是去重,比如统计测试用例的标签去重,直接set(tags)就完了。这四个类型在测试脚本里的分工很清楚:列表管批量,字典管结构化数据,元组管固定配置,集合管去重。实际写代码时,列表和字典占了九成使用场景,元组和集合知道大概用途就行。
3.4 字符串处理是断言前的必修课
接口返回里的多余空格、换行,URL参数拼接,这些都需要处理字符串。最常用的操作就几个:strip()去掉首尾空白,split()按分隔符切分,replace()替换,startswith()判断开头,以及f-string格式化。
name = " alice " clean_name = name.strip() # "alice" parts = "username=alice&password=123456".split("&") # ["username=alice", "password=123456"] msg = f"用例执行失败,响应码是{code}"f-string在拼接测试日志和断言信息时太好用了,比一堆加号拼字符串清爽得多。记住一点:任何从外部读进来的数据,在用于断言之前都要确认它的格式,该strip的strip,该split的split。字符串处理是那种看起来简单、实际最容易藏Bug的地方,多一个空格、多一个换行都可能让断言失败。
4. 流程控制就是脚本的指挥棒:条件、循环与重试逻辑
4.1 用if/elif/else做环境判断和参数校验
测试脚本里条件判断最常见的三个场景:根据环境切换地址,判断参数是否为空,根据响应码走不同分支。
env = "test" if env == "test": base_url = "https://test.api.example.com" elif env == "prod": base_url = "https://api.example.com" else: raise ValueError(f"未知环境: {env}")这里有个小习惯值得养成:所有假设之外的情况,用else抛异常,而不是默默跳过。不然脚本在错误环境里跑了半天,最后结果全错,你还不知道是配置问题。参数校验也一样,比如接口要求username不能为空,如果用例数据里漏了,直接在脚本入口抛错比等到发请求后看一堆堆栈更省时间。
4.2 for循环写批量用例执行
自动化测试的核心就是"批量重复",for循环是绝对的主角。遍历列表跑用例:
for case in test_cases: response = send_request(case) check_result(case, response)如果同时需要序号,用enumerate:
for index, case in enumerate(test_cases, start=1): print(f"开始执行第{index}条用例")range()配合len()用在大批量数据上也很常见。注意一点:循环里如果某条用例失败,不要在循环里擅自break,除非你明确知道后面数据都依赖前面数据。否则一条挂了后面全不跑,测试结果就不完整了。更稳妥的做法是记录失败用例,循环结束后统一展示,这样一次能看到所有问题。
4.3 while循环做超时重试
接口自动化里经常遇到网络抖动,第一次请求超时,重试一次就好了。用while实现一个简单的重试逻辑:
max_retries = 3 for attempt in range(1, max_retries + 1): try: response = requests.post(url, json=payload, timeout=5) break except requests.exceptions.Timeout: print(f"第{attempt}次请求超时,重试") else: raise RuntimeError(f"重试{max_retries}次仍然失败")这里用了for的else分支,循环正常结束没break说明一直失败,抛异常。比while少写一个计数器,也更不容易出现死循环。重试之间最好加一个短暂停顿,time.sleep(1),避免对服务造成过大压力。这个模式在测试脚本里非常实用,建议直接复制到自己的工具函数里保存起来,以后凡是涉及网络请求都能复用。
5. 函数与模块:把重复步骤变成可复用能力
5.1 函数是脚本可维护性的分水岭
很多脚本刚开始只有几十行,全是顺序执行的代码,跑得通就完事。等用例多了就发现,同一个登录步骤在十个用例里复制粘贴了十遍,后面接口字段一改,要改十个地方。函数就是解决这个问题的。
def get_headers(token): return {"Authorization": f"Bearer {token}"} def post_json(url, payload, timeout=5): response = requests.post(url, json=payload, timeout=timeout) response.raise_for_status() return response.json()定义函数时可以用默认参数,比如timeout=5,调用时不传就用默认值。这在实际测试里非常实用,大部分接口5秒够用,个别慢接口调用时单独传timeout=10即可。函数命名也值得花心思,最好做到看到名字就知道干什么,比如get_headers明显是取请求头,post_json明显是发送JSON请求,半年后再回来看代码也不用猜。
5.2 参数传递:位置参数、关键字参数、可变参数
Python函数调用时,可以直接按顺序传位置参数,也可以用关键字参数指明传给谁:
post_json("/api/login", {"username": "alice"}, timeout=3)可读性更强的是全部用关键字参数调用。再看一个特殊场景:有一段逻辑要适配不同数量的测试数据,可以用*args把多余的位置参数收成元组,**kwargs把多余的关键字参数收成字典。框架代码里很常见,入门阶段知道有这个东西、看代码不懵就行。日常写测试脚本时,能用普通参数解决的场景占绝大多数,可变参数更多是阅读别人代码时需要理解的概念。
5.3 模块和import:把公共代码拆出去
写测试脚本最大的误区是所有的东西堆在一个文件里。合理的做法是拆分职责:公共请求方法放common.py,测试数据放配置文件,用例分文件存放。引用公共模块用import:
from common import post_json, get_headersPython的import规则不用一次记全,记住最常用的几条:同一目录下的模块可以直接from xxx import yyy;不同目录建议把公共代码做成包,或者通过sys.path调整路径,但尽量少用。每个测试文件末尾可以加上ifname== "main":这一行,作用是直接运行这个文件时执行里面的调试代码,被其他模块导入时不执行,非常适合在写用例时快速自测。我自己的习惯是每个py文件都保留这个入口,写完一个函数立刻跑一下验证,避免攒了一堆代码最后一起调错。
6. 文件读写与路径处理:读取用例、保存报告
6.1 with open和编码问题
测试数据往往不在代码里写死,而是放在外部文件里,所以文件读写是刚需。读文本最标准的写法:
with open("test_data.txt", "r", encoding="utf-8") as f: content = f.read()with语句会在代码块结束时自动关闭文件,不用手动f.close()。编码问题特别坑:Windows默认可能用GBK,而文件是utf-8,打开就乱码或抛UnicodeDecodeError。凡是自己创建的文件,写入和读取都明确指定encoding="utf-8",能省掉一堆麻烦。写完文件后如果再读出来,遇到中文乱码,先别怀疑代码逻辑,看一眼两侧的编码参数是否一致,往往就是差这一个参数。
6.2 读取JSON、CSV和配置文件
接口测试最常见的用例存储格式是JSON,读取很直接:
import json with open("cases.json", "r", encoding="utf-8") as f: cases = json.load(f) for case in cases: print(case["name"])CSV文件用csv模块:
import csv with open("users.csv", "r", encoding="utf-8") as f: rows = list(csv.DictReader(f))配置文件用pyyaml读取,得到的同样是字典。这些模块的用法不必死记,但要知道数据从文件到Python对象基本都是这个套路:打开文件,交给对应解析器,得到一个列表或字典,然后就可以用前面学过的数据结构操作去处理。把测试数据和脚本分离有一个额外好处:测试人员修改用例不用碰代码,减少误改逻辑的风险。
6.3 用pathlib解决路径问题
初学者最常见的报错之一是FileNotFoundError: [Errno 2] No such file or directory。不是文件不存在,而是运行时当前目录和文件所在目录不一致。命令行里在项目根目录运行和从子目录运行,相对路径结果完全不同。解决办法是使用基于当前脚本文件位置的绝对路径:
from pathlib import Path base_dir = Path(__file__).resolve().parent data_path = base_dir / "data" / "cases.json" cases = json.loads(data_path.read_text(encoding="utf-8"))Path("data") / "cases.json"就是路径拼接的Python写法,比手写字符串拼接多一些跨平台优势,在Windows和Linux上都不会错。把这个写成一个工具函数,后面所有测试文件都用它定位数据文件和报告输出目录,路径问题基本绝迹。我见过太多脚本在本地跑得好好的,一放到Jenkins或者别人电脑上就报找不到文件,几乎都是因为用了相对路径。
7. 异常处理和断言:脚本挂了要知道挂在哪
7.1 try/except/finally的基本结构
测试脚本运行过程中必然遇到异常:网络不通、接口返回格式变化、文件不存在。不处理异常,脚本会直接中断;处理不好,又容易把错误吞掉、让人不知道失败原因。标准结构是:
try: response = requests.post(url, json=payload, timeout=5) except requests.exceptions.Timeout: print("请求超时,需要重试") except requests.exceptions.ConnectionError: print("网络连接失败") finally: print("这段无论如何都会执行")finally适合放清理动作,比如关闭资源。Python还有一个else子句表示没有异常时执行的代码,但不一定每次都用。核心原则:捕获异常时要明确类型,不要一上来就except Exception把什么都吞掉,那样出问题时日志里只有一句"出错",完全没法排查。写except的时候可以连异常对象一起接住:except requests.exceptions.Timeout as e,在日志里把实际的异常信息打出来。
7.2 让错误信息说人话
测试脚本的价值在于失败时能快速定位。围绕这个目标,有几个实操建议:
- 记录上下文:catch到异常后,把当前用例名、URL、参数一起打印或写入日志,否则你只知道"报错了",不知道哪条用例在哪里报错。
- 使用logging而不是print:print输出难以分级,logging可以分debug、info、error,并按时间格式化。
import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) try: logger.info("开始执行用例: %s", case["name"]) run(case) except Exception as e: logger.error("用例 %s 执行失败: %s", case["name"], e)我建议从第一天写脚本就用logging,哪怕现在只是打印一行信息,也比print强。等用例规模上来之后,看时间戳、看日志级别、筛错误信息,这些能力全都要靠logging。
7.3 主动raise和断言:把"不符合预期"变成异常
测试里有个概念叫断言,本质是"如果条件不满足,就认为这次测试失败"。pytest里直接写assert:
def test_login_success(): resp = post_json("/api/login", {"username": "alice", "password": "123456"}) assert resp["code"] == 200 assert resp["data"]["token"], "token不能为空"assert后面加描述信息是个好习惯,失败时能看到原因。有些情况下需要主动抛异常,比如配置缺失、前置条件不满足,直接raise ValueError("缺少必要的配置项"),让脚本尽快停下来,而不是带着错误状态继续跑导致大量无效用例。有些入门同学怕抛异常,总觉得异常是坏事,其实在测试脚本里,异常恰恰是传递"这里有问题"信号的标准方式,用例失败就是要把问题暴露出来。
8. 从脚本到框架:用类和对象组织测试代码
8.1 类是什么:把数据和操作打包在一起
当函数越拆越散,参数总是来回传的时候,就该考虑用类了。类可以把状态和方法放在一起。举个例子,一个API测试客户端:
class ApiClient: def __init__(self, base_url, token=""): self.base_url = base_url self.token = token def post(self, path, payload): headers = {"Authorization": f"Bearer {self.token}"} return requests.post(self.base_url + path, json=payload, headers=headers, timeout=5)用的时候先创建对象:
client = ApiClient("https://api.example.com", token="abc") resp = client.post("/api/login", {"username": "alice", "password": "123456"})__init__是初始化方法,接受创建对象时的参数。self代表这个对象本身,类里的每个方法第一个参数都是self,否则无法访问实例的base_url和token。类与函数最大的区别就在这里:函数每次调用都要重新传参,而对象创建之后,base_url和token就一直跟着这个对象走,后面的方法调用不需要再重复传。
8.2 用类把公共操作收敛起来
适合用类组织的测试代码包括:请求封装、数据库连接、文件读取工具、UI页面的元素操作。一个典型思路是创建一个BaseApi类,所有接口模块继承它复用请求方法,子类只写业务相关的接口方法。这样公共逻辑只维护一份,修改base_url或加统一鉴权时,只改一处。
举一个我在实际项目中常用的做法:在BaseApi的__init__里把base_url、token、请求会话都准备好,子类继承后只用写业务方法:
class UserApi(BaseApi): def login(self, username, password): return self.post("/api/login", {"username": username, "password": password})这样测试用例调用的时候,一行代码就能发起请求,读起来非常清晰。框架能提升效率的原因不是用了多高级的语法,而是把重复的东西收拢到了一处。
8.3 继承和Page Object雏形
UI自动化里的Page Object模式,本质上就是类和继承的一种应用。每个页面建一个类,把页面上的元素定位和操作封装成方法:
class LoginPage: def __init__(self, driver): self.driver = driver def input_username(self, name): self.driver.find_element("id", "username").send_keys(name) def click_login(self): self.driver.find_element("id", "login_btn").click()再用继承定义一个和业务相关的用例页面类,把公共导航逻辑放在基类。看起来比函数复杂,但带来的好处是清晰的对应关系:页面和数据模型变成代码里的对象,测试用例读起来接近自然语言。入门阶段先掌握class、init、self、继承这四个概念,就已经足够把脚本向框架推进一大步。先别急着追设计模式,能把代码从一堆散函数整理成几个类,对测试脚本来说已经是一次质变。
9. 一个完整的登录接口自动化脚本:从零到能跑
9.1 需求与设计
用一个最小但完整的例子把这些语法串起来:登录接口自动化脚本。目标是对登录成功、密码错误、用户不存在三条用例跑一遍,输出通过或失败。目录结构:
login_test/ ├── cases.json └── test_login.pycases.json存测试数据,test_login.py读取数据、循环执行、断言、输出结果。这个结构虽然简单,但已经包含了数据与脚本分离、函数拆分、日志输出这些工程习惯,后面往pytest框架迁移也很自然。
9.2 完整代码与解读
先看测试数据文件:
[ { "name": "登录成功", "username": "alice", "password": "123456", "expected": 200 }, { "name": "密码错误", "username": "alice", "password": "wrong", "expected": 401 }, { "name": "用户不存在", "username": "nonexist", "password": "123456", "expected": 404 } ]然后是脚本:
import json import logging from pathlib import Path import requests logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) BASE_URL = "https://api.example.com" def load_cases(): path = Path(__file__).resolve().parent / "cases.json" with open(path, "r", encoding="utf-8") as f: return json.load(f) def login(username, password): resp = requests.post(f"{BASE_URL}/api/login", json={"username": username, "password": password}, timeout=5) return resp.status_code def run_case(case): actual = login(case["username"], case["password"]) if actual == case["expected"]: logger.info("用例 %s 通过", case["name"]) else: logger.error("用例 %s 失败,期望 %s,实际 %s", case["name"], case["expected"], actual) raise AssertionError(f"断言失败: {case['name']}") def run_all(): cases = load_cases() for case in cases: run_case(case) if __name__ == "__main__": run_all()这段代码用到了前面所有章节的语法:Path处理路径、json读取数据、for循环批量执行、函数封装、f-string和日志格式化、断言失败抛异常。直接把同一个BASE_URL改成真实地址,就能跑起来尝试。我把断言失败的方式设计成抛异常,是为了让脚本在碰到失败用例时立刻停下,实际工作中如果希望跑完所有用例再汇总,可以把失败结果收集到列表里,循环结束后统一打印。
9.3 运行与常见问题
命令行进入login_test目录,激活虚拟环境后执行:
python test_login.py如果要用pytest组织,把run_case改成以test_开头的函数,pytest会自动发现:
python -m pytest test_login.py -v常见问题就三个:一是requests没有安装,报ModuleNotFoundError,在虚拟环境里python -m pip install requests即可;二是路径不对,确认脚本路径不是从其他目录用相对路径硬拼的;三是编码问题,打开/写入文件统一用encoding="utf-8"。把这三个问题记牢,脚本跑不通时按顺序排查,基本都能解决。如果pytest报告里显示collected 0 items,检查一下文件名是不是以test_开头,函数名是不是以test_开头,以及是否在正确的目录下执行。
带过不少刚转自动化的测试同学,我发现最容易出现的结果不是学不会,而是"学太多但用不上"。有人花两周啃装饰器、协程,回头发现自己连一条接口用例都还没完整跑通过。真正有效的路线,是先把这篇文章里的语法用到一个能跑的脚本里,哪怕只是把手工点在Postman里的请求变成Python脚本,就已经迈出了很大一步。之后再加数据驱动、加报告、加CI集成,每一步都是在一个能运行的基础上做增量。遇到报错不要慌,先读最后一行,再往上看具体是哪一行代码触发的,绝大多数语法问题都能在十分钟内解决。祝你的第一条测试脚本早日跑通。