软件测试从入门到精通 · 第29篇
如果你准备踏入自动化测试领域,那么Python是自动化测试领域的主流语言。它语法简洁、生态丰富、学习成本低,Selenium、pytest、requests、Appium等主流测试工具对Python均有良好支持。
但很多Python教程的问题是太全面,一上来就从计算机原理讲到设计模式,新手学了两周还没写到第一个测试脚本。所以,本文只讲测试工作中真正会用到的东西,够用即可,需要时再深入学习。
本文假设你有基本的编程概念(比如知道什么是变量),如果尚未接触也不必担心,每个知识点都会附上简短代码示例。
一、变量与数据类型
Python是动态类型语言,变量不需要声明类型,直接赋值即可。
1.1 字符串(str)
字符串是测试中常用的数据类型,URL、页面标题、元素文本、断言信息,都属于字符串类型。Python提供了丰富的字符串操作方法,以下是一些常用的。
# 字符串的四种写法url = "https://www.example.com" # 双引号title = '登录页面' # 单引号(效果一样)html = """<div> <p>多行文本用三引号</p></div>""" # 三引号可以跨行# 常用操作text = " Hello Selenium "print(text.strip()) # "Hello Selenium"—去首尾空格print(text.upper()) # " HELLO SELENIUM "—全大写print(text.lower()) # " hello selenium "—全小写print("Selenium" in text) # True — 判断是否包含print(text.replace("Selenium", "Playwright")) # 替换print(text.split()) # ['Hello', 'Selenium']—按空格分割# f-string:测试日志中常用的格式化方式name = "admin"password = "123456"print(f"正在登录,用户名:{name},密码:{password}")# f-string调试语法(Python 3.8+):f"{变量=}"直接打印"变量名=值"print(f"{name=}") # name='admin'print(f"{password=}") # password='123456'
1.2 整数与浮点数(int/float)
测试中整数和浮点数主要用于计数、超时设置、价格比较等场景。Python支持类型之间的相互转换,这在解析测试数据时经常用到。
# 整数count = 10retry_times = 3# 浮点数price = 19.99timeout = 30.5# 类型转换(测试中经常用到)num_str = "100"real_num = int(num_str) # 字符串转整数->100float_num = float("3.14") # 字符串转浮点数->3.14str_num = str(100) # 整数转字符串->"100"
1.3 列表(list)
列表是Python较为常用的数据结构,测试数据驱动、元素集合管理都离不开它。下面演示列表的增删改查、切片遍历和推导式等核心操作。
# 创建列表test_cases = ["登录成功", "用户名错误", "密码为空"]numbers = [1, 2, 3, 4, 5]mixed = ["admin", 123, True] # 可以混放不同类型# 增删改查test_cases.append("验证码错误") # 末尾追加test_cases.insert(1, "密码错误") # 指定位置插入test_cases.remove("用户名错误") # 按值删除deleted = test_cases.pop() # 删除并返回最后一个test_cases[0] = "登录正常" # 修改指定位置# 切片:列表[起始:结束:步长]print(test_cases[1:3]) # 第2到第3个元素print(test_cases[::-1]) # 倒序# 遍历列表for case in test_cases: print(f"执行测试用例:{case}")# 列表推导式:一行代码生成列表squares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
1.4 字典(dict)
字典是键值对结构,测试中用于存储配置、测试数据、请求参数等。与列表相比,字典通过键名(而不是数字索引)来存取数据,代码可读性更高。
# 创建字典test_data = { "username": "admin", "password": "123456", "expected_title": "管理后台"}# 读取print(test_data["username"]) # adminprint(test_data.get("email", "无")) # 无。键不存在时返回默认值# 修改与新增test_data["password"] = "new_pass" # 修改test_data["remember"] = True # 新增# 遍历for key, value in test_data.items(): print(f"{key}: {value}")# 嵌套结构(测试中常用)test_suite = { "suite_name": "登录模块", "cases": [ {"id": "TC001", "title": "正常登录", "data": {"user": "admin", "pwd": "123"}}, {"id": "TC002", "title": "密码错误", "data": {"user": "admin", "pwd": "999"}}, ]}print(test_suite["cases"][0]["data"]["user"]) # admin# 字典合并(Python 3.9+):|运算符,等价于{**a, **b}config_a = {"browser": "Chrome", "timeout": 30}config_b = {"headless": True, "timeout": 15} # 重复键:后面覆盖前面merged = config_a | config_b # {'browser': 'Chrome', 'timeout': 15, 'headless': True}
1.5 元组(tuple)
元组是不可变的有序序列,创建后不能增、删、改,适合存储不会变化的常量配置。
# 创建元组browser_types = ("Chrome", "Firefox", "Edge")screen_size = (1920, 1080)# 元组拆包width, height = screen_sizeprint(f"宽度:{width},高度:{height}")
为什么用元组?
作为字典的键、函数返回多个值、保护数据不被意外修改。
二、运算符与条件判断
2.1 运算符
Python运算符分为几个大类。算术运算符处理数字计算,比较运算符用于断言判断,逻辑运算符组合多个条件,成员运算符检查元素是否在容器中。这些是编写测试逻辑的基础。
# 算术运算符a, b = 10, 3print(a + b) # 13 加print(a - b) # 7 减print(a * b) # 30 乘print(a / b) # 3.333... 除(浮点数)print(a // b) # 3 整除print(a % b) # 1 取余print(a ** b) # 1000 幂运算# 比较运算符。测试断言的基础print(5 == 5) # True 等于print(5 != 3) # True 不等于print(5 > 3) # True 大于print(5 < 3) # False 小于print(5 >= 5) # True 大于等于print(5 <= 3) # False 小于等于# 逻辑运算符。组合多个条件logged_in = Trueis_admin = Falseprint(logged_in and is_admin) # False 且print(logged_in or is_admin) # True 或print(not logged_in) # False 非# 成员运算符。判断元素是否在容器中print("admin" in ["admin", "guest"]) # Trueprint("error" not in "success message") # True
2.2 条件判断(if-elif-else)
条件判断是编写测试逻辑的核心,例如根据执行结果进行不同的断言。
# 基本结构status_code = 200if status_code == 200: print("请求成功")elif status_code == 404: print("页面不存在")elif status_code == 500: print("服务器内部错误")else: print(f"未知状态码:{status_code}")# 三元表达式(一行if-else)result = "通过" if status_code == 200 else "失败"# 实战:根据页面元素判断登录结果def check_login(page_source): if "欢迎回来" in page_source: return "登录成功" elif "用户名或密码错误" in page_source: return "账号密码错误" elif "验证码错误" in page_source: return "验证码错误" else: return "未知错误"# 嵌套条件response_text = '{"code": 200, "message": "success"}' # 模拟接口响应内容(JSON格式字符串)if status_code == 200: if "success" in response_text: print("操作成功") else: print("返回成功但操作未生效")
三、循环
3.1 for循环
for循环用于遍历可迭代对象,在测试中常用于遍历测试数据、元素列表等。除了基本的列表遍历,Python还提供了range()、enumerate()、zip()等辅助函数,让循环更加灵活。
# 遍历列表elements = ["用户名输入框", "密码输入框", "登录按钮"]for element in elements: print(f"正在定位:{element}")# 遍历字典config = {"browser": "Chrome", "timeout": 30, "headless": True}for key, value in config.items(): print(f"{key} = {value}")# range()函数for i in range(5): # 0, 1, 2, 3, 4 print(f"第 {i+1} 次重试")for i in range(2, 6): # 2, 3, 4, 5 print(i)for i in range(0, 10, 2): # 0, 2, 4, 6, 8(步长2) print(i)# enumerate():同时获取索引和值test_cases = ["登录", "注册", "找回密码"]for index, case in enumerate(test_cases): print(f"用例 {index+1}:{case}")# zip():同时遍历多个列表urls = ["/login", "/register", "/forgot"]titles = ["登录页面", "注册页面", "找回密码"]for url, title in zip(urls, titles): print(f"打开 {url},预期标题:{title}")# break与continuefor case in test_cases: if case == "注册": continue # 跳过注册用例 if case == "找回密码": break # 到这就停止 print(f"执行:{case}")
3.2 while循环
while循环在测试中常用于等待、重试等需要满足某个条件才停止的场景。与for循环不同,while不依赖可迭代对象,只依赖条件表达式。
# 基本while循环count = 0while count < 3: print(f"第 {count+1} 次重试") count += 1# 实战:重试机制import timedef wait_for_element(driver, locator, timeout=10): """等待元素出现,最多等待timeout秒""" start_time = time.time() while time.time() - start_time < timeout: try: element = driver.find_element(*locator) return element except Exception: time.sleep(0.5) raise TimeoutError(f"等待超时:{locator}")
四、函数
函数是代码复用的基本单位。测试框架中,测试用例本身就是函数。
4.1 基本定义与调用
函数通过def关键字定义。参数可以设置默认值,调用时可以选择按位置传参或按关键字传参,非常灵活。
# 知识点1:用def定义函数。def函数名(参数): 函数体def greet(): print("Hello, Tester!")greet() # 知识点2:调用函数。函数名(),无参数就写空括号# 知识点3:有参函数+return。函数用return把结果交还给调用处def login(username, password): print(f"用户名:{username},密码:{password}") return f"{username} 登录成功"result = login("admin", "123456") # 知识点4:位置传参。按定义顺序依次传入print(result) # result接住了return返回的值# 知识点5:默认参数。定义时给参数赋默认值,调用时可以省略def open_browser(browser="Chrome", headless=False): print(f"启动 {browser} 浏览器,无头模式:{headless}")open_browser() # 知识点6:不传参→全部用默认值(Chrome, False)open_browser("Firefox", True) # 知识点7:位置传参→按顺序覆盖(Firefox, True)open_browser(headless=True) # 知识点8:关键字传参→"参数名=值",可只传部分、顺序随意# 知识点9:类型注解(Python 3.5+,3.9+支持内置泛型list[str]/dict[str, int])# 现代Python项目的标准做法,标注参数和返回值类型,提升可读性、IDE提示与静态检查def login(username: str, password: str) -> str: """登录(带类型注解:参数为str,返回str)""" return f"{username} 登录成功"
4.2 可变参数
当不确定函数需要接收多少个参数时,可以用*args和**kwargs。*args将多个位置参数打包成元组,**kwargs将多个关键字参数打包成字典。
# 知识点1:*args。形参前加一个*,把任意多个位置参数打包成元组def run_cases(*case_names): for name in case_names: # case_names是元组 ("TC001","TC002","TC003") print(f"执行用例:{name}")run_cases("TC001", "TC002", "TC003") # 传几个都行,都会被收进元组# 知识点2:**kwargs。形参前加两个*,把任意多个关键字参数打包成字典def set_config(**kwargs): for key, value in kwargs.items(): # kwargs是字典 {"browser":"Chrome", ...} print(f"配置 {key} = {value}")set_config(browser="Chrome", timeout=30, base_url="https://test.com")
4.3 返回值
函数用return返回结果。Python函数可以返回多个值,实际是打包成一个元组,调用时可以用元组拆包分别接收。
# 知识点1:return返回多个值。实际是打包成一个元组返回def get_bounds(element): x = element.location['x'] y = element.location['y'] width = element.size['width'] height = element.size['height'] return x, y, width, height # 等价于return (x, y, width, height)# 模拟一个页面元素(实际使用中是Selenium定位到的WebElement)class MockElement: location = {'x': 10, 'y': 20} size = {'width': 800, 'height': 600}some_element = MockElement()# 知识点2:元组拆包。用"变量1, 变量2, ..."按位置接收返回的每个值x, y, w, h = get_bounds(some_element)print(f"元素位置:({x}, {y}),尺寸:{w}x{h}")
4.4 lambda表达式
lambda是匿名函数,适合在需要函数对象但逻辑非常简单时使用。测试中常见于sorted排序的key参数、filter/map等场景。
# 知识点1:lambda语法。lambda 参数: 表达式,返回表达式的结果,适合一行能写下的简单逻辑# 下面的key=lambda x: x["duration"]等价于定义函数def f(x): return x["duration"]test_results = [ {"name": "TC001", "duration": 3.2}, {"name": "TC002", "duration": 1.5}, {"name": "TC003", "duration": 4.8},]# 知识点2:sorted的key参数。指定按什么排序,这里按字典中duration的值升序sorted_results = sorted(test_results, key=lambda x: x["duration"])print(sorted_results) # 按耗时排序:TC002(1.5)→TC001(3.2)→TC003(4.8)# 知识点3:filter(函数, 列表)。用函数逐个判断,保留返回True的元素numbers = [1, 2, 3, 4, 5, 6]evens = list(filter(lambda x: x % 2 == 0, numbers)) # 保留偶数->[2, 4, 6]# 知识点4:map(函数, 列表)。用函数逐个转换每个元素doubled = list(map(lambda x: x * 2, numbers)) # 每个数乘2-> [2, 4, 6, 8, 10, 12]
五、类与对象
测试中类常用于封装测试用例、测试数据等对象,比如把一条测试用例抽象成一个类,方便统一管理和复用。不需要深入理解面向对象理论,先掌握类的定义、属性和方法即可。
# 知识点1:用class定义类(类名习惯首字母大写)class TestCase: """测试用例类""" # 知识点2:类变量。定义在类里、所有实例共享同一份 category = "功能测试" # 知识点3:构造方法__init__。创建实例时自动调用,self表示当前这个实例 def __init__(self, case_id, title): self.case_id = case_id # 知识点4:实例变量。每个实例各有一份,通过 self 存取 self.title = title self.status = "未执行" # 知识点5:方法。类里的函数,第一个参数固定是self,用self.xxx访问实例数据 def run(self): """执行用例(示例中模拟执行)""" print(f"正在执行:{self.case_id}{self.title}") self.status = "通过" # 在方法内修改实例变量 def report(self): """返回用例结果""" return f"[{self.status}] {self.case_id} - {self.title}"# 知识点6:实例化。"类名(参数)"创建对象,参数会传给__init__case1 = TestCase("TC001", "正常登录")case1.run() # 知识点7:调用方法。"实例.方法名()"print(case1.report())case2 = TestCase("TC002", "密码错误") # 每个实例的实例变量互相独立print(case2.report()) # 未执行,状态为"未执行"# 知识点8:继承。"class 子类(父类)",子类自动拥有父类的属性和方法class ApiTestCase(TestCase): """接口测试用例(继承TestCase)""" category = "接口测试" # 知识点9:子类覆盖类变量(重新赋值即可) # 知识点10:super().__init__(...)。调用父类的构造方法,复用父类初始化逻辑 def __init__(self, case_id, title, method, path): super().__init__(case_id, title) self.method = method # 知识点11:子类新增自己的实例变量 self.path = path # 知识点12:方法重写。与父类方法同名,子类实例调用时优先用子类的版本 def run(self): """重写父类方法""" print(f"发送 {self.method} 请求:{self.path}") self.status = "通过"api_case = ApiTestCase("TC010", "查询用户列表", "GET", "/api/users")api_case.run() # 实际调用的是ApiTestCase自己的run()print(api_case.report()) # report()未重写,沿用父类的实现print(f"用例分类:{TestCase.category} / {ApiTestCase.category}") # 类变量可直接用"类名.变量"访问
六、模块与异常处理
6.1 模块导入(import)
Python通过import语句引入其他模块的功能。模块可以是Python标准库(如 time)、第三方库(如selenium)、或你自定义的.py文件。常见的导入方式有以下几种。
# 导入整个模块import timetime.sleep(2)# 导入模块并起别名import selenium.webdriver as wd # 之后可用wd.Chrome(),效果同from selenium import webdriverfrom selenium.webdriver.common.by import By# 从模块导入特定内容from selenium import webdriverfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as EC# 导入自定义模块(同目录下的py文件)from login_page import LoginPagefrom test_data import user_data# 按目录导入from pages.login_page import LoginPagefrom utils.config import BASE_URL
6.2 异常处理(try-except)
自动化测试中,网络超时、元素找不到、断言失败都是异常。合理处理异常可以提高脚本的健壮性。Python的异常处理包括try、except、else、finally四个子句,以及自定义异常。
# 知识点1:try-except基本结构。把可能出错的代码放try,一旦出错就跳到except处理try: price = int("19.99") # "19.99"不是合法的整数字符串,会抛ValueError print(f"价格:{price}")except Exception as e: print(f"转换失败:{e}") # 捕获任意异常,e是错误信息# 知识点2:捕获特定异常。按异常类型分别处理,except从上往下匹配第一个try: data = {"username": "admin"} print(data["password"]) # 键不存在,抛KeyErrorexcept KeyError: print("缺少键:password") # 只处理KeyErrorexcept ValueError: print("值不合法")except Exception as e: print(f"未预期的错误:{type(e).__name__} - {e}")# 知识点3:else。try没有异常时才执行else里的代码# 知识点4:finally。无论有没有异常都会执行,是清理资源的固定位置try: result = 10 / 2 # 正常执行,不会触发exceptexcept ZeroDivisionError: print("除数不能为零")else: print(f"计算结果:{result}") # try成功时执行finally: print("清理资源") # 无论是否异常都会执行# 知识点5:自定义异常。继承Exception,用于表达业务上的错误情况class TestDataError(Exception): """测试数据异常""" passdef validate_test_data(data): if "username" not in data: raise TestDataError("缺少必填字段:username") # raise主动抛出异常validate_test_data({"password": "123"}) # 会抛出TestDataError# 知识点6:安全地取值,键不存在时返回默认值而不是让程序崩溃def safe_get(dict_data, key, default="未找到"): try: return dict_data[key] except KeyError: return defaultuser = {"username": "admin"}print(safe_get(user, "email")) # 未找到(键不存在,不崩溃)print(safe_get(user, "username")) # admin
七、文件与数据格式
测试中经常需要读取测试数据文件或写入测试结果日志。
7.1 文本文件
open()加with语句是Python读写文件的标准方式。with会自动管理文件关闭,即使发生异常也能正确释放资源。
# 知识点1:with open(文件名, 模式, 编码) as f,上下文管理器,用完自动关闭文件,即使出异常也会释放资源# 知识点2:模式"w"写入(会覆盖原文件已有内容,文件不存在则创建)# 写入文件with open("test_result.txt", "w", encoding="utf-8") as f: f.write("测试报告\n") f.write("=" * 40 + "\n") f.write("用例总数:10\n") f.write("通过:8\n") f.write("失败:2\n")# 知识点3:模式"a"追加(在文件末尾续写,不清空原内容,文件不存在则创建)# 追加写入with open("test_log.txt", "a", encoding="utf-8") as f: f.write(f"[2024-01-15 10:30:00] 执行登录用例\n")# 知识点4:模式"r"读取+f.read()一次性读出整个文件内容# 读取整个文件with open("test_result.txt", "r", encoding="utf-8") as f: content = f.read() print(content)# 知识点5:逐行读取,for line in f按行遍历,line自带结尾换行符# 知识点6:line.strip()去掉每行首尾空白(换行符等);if case_name跳过空行# 逐行读取with open("test_cases.txt", "r", encoding="utf-8") as f: for line in f: case_name = line.strip() if case_name: # 跳过空行 print(f"执行:{case_name}")
7.2 文件模式速查表
r:只读,文件不存在时报错。
w:写入(覆盖),文件不存在时创建新文件。
a:追加,文件不存在时创建新文件。
r+:读写,文件不存在时报错。
b:二进制模式(与上述模式组合使用,如'rb'或'wb')。
7.3 JSON处理
JSON是测试中最常见的数据交换格式,API测试的请求和响应基本都是 JSON。Python内置的json模块提供了dumps(字典→JSON字符串)和 loads(JSON字符串→字典)两种核心方法,以及对应的文件读写方法dump和load。
import json# 知识点1:dumps,Python字典->JSON字符串(用于打印、拼接、传输)# 知识点2:ensure_ascii=False让中文原样输出(否则转成\uXXXX);indent=2缩进美化test_data = { "username": "admin", "password": "123456", "remember": True, "roles": ["admin", "editor"]}json_str = json.dumps(test_data, ensure_ascii=False, indent=2)print(json_str)# 输出:# {# "username": "admin",# "password": "123456",# "remember": true,# "roles": [# "admin",# "editor"# ]# }# 知识点3:dump,把字典直接写入JSON文件(dumps返回字符串、dump写入文件,注意多一个s的区别)# 写入JSON文件(dump,生成test_data.json)with open("test_data.json", "w", encoding="utf-8") as f: json.dump(test_data, f, ensure_ascii=False, indent=2)# 知识点4:load,从JSON文件读回字典(dump和load成对,配合文件使用)# 读取JSON文件(load,读回字典)with open("test_data.json", "r", encoding="utf-8") as f: loaded_data = json.load(f)print(loaded_data["username"]) # "admin"print(loaded_data["roles"]) # ['admin', 'editor']# 知识点5:loads,JSON字符串->Python字典(解析接口响应的标准动作)# JSON字符串->Python字典(loads,用于解析接口响应)response = '{"code": 200, "message": "success", "data": {"token": "abc123"}}'data = json.loads(response)print(data["code"]) # 200print(data["data"]["token"]) # abc123
7.4 CSV处理
CSV是测试数据驱动的常用格式,可以方便地用Excel编辑。Python的csv模块提供了DictReader(将每行读为字典)和writer(写入行数据)两种核心工具。
import csv# 知识点1:csv.writer+writerows,把列表的列表逐行写入文件# 知识点2:newline=""必须加,Windows下不加会在每行后多出空行# 知识点3:第一行为表头,DictReader靠它识别列名# 写入CSV文件(生成test_users.csv,第一行为表头)users = [ ["username", "password"], # 表头,DictReader靠它识别列名 ["admin", "123456"], ["tester", "abc123"],]with open("test_users.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerows(users)# 知识点4:csv.DictReader,把每行读成字典,用表头当键名(row['username']这种取法)# 读取CSV文件with open("test_users.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) # 每行变为字典 for row in reader: print(f"用户名:{row['username']},密码:{row['password']}") # 这里执行具体的测试逻辑# 知识点5:数据驱动:遍历行+条件判断+异常处理+字典解包合并def test_login_from_csv(csv_path): with open(csv_path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) results = [] for row in reader: try: # 执行登录操作(这里简化演示) if row["username"] and row["password"]: result = "通过" else: result = "失败" except Exception as e: result = f"异常:{e}" results.append({**row, "result": result}) # {**row, ...}把result合并进原字典 return results# 调用函数print(test_login_from_csv("test_users.csv"))
八、包管理与综合实战
8.1 pip安装第三方库
Python的强大在于生态,测试中要用到的库基本都通过pip安装。pip是Python的包管理工具,可以安装、查看、卸载第三方库。推荐将项目依赖写入requirements.txt统一管理。
# 基本安装pip install selenium# 安装指定版本pip install selenium==4.47.0# 批量安装(依赖的包写在requirements.txt)pip install -r requirements.txt# 查看已安装的库pip list# 查看某个库的信息pip show selenium# 卸载pip uninstall selenium
测试工程常用的requirements.txt示例:
selenium>=4.47.0pytest>=9.1.1pytest-html>=4.2.0pytest-xdist>=3.8.0pytest-rerunfailures>=16.5requests>=2.34.2webdriver-manager>=4.1.2openpyxl>=3.1.5PyMySQL>=1.2.0allure-pytest>=2.16.0
8.2 综合实战:一个可运行的登录测试脚本
下面将这些知识点整合起来,编写一个纯Python可运行的测试脚本,综合演示:字典配置、函数封装、CSV数据驱动、循环遍历测试用例、条件判断做断言、异常处理、JSON生成测试报告。脚本不依赖浏览器和网络,被测对象是一个模拟登录服务,把前面学的基础知识串成一条完整的测试流程,复制到本地就能跑起来。
配套两个文件(放在同一目录,运行python test_login.py):
"""综合实战:一个纯Python可运行的登录测试脚本演示:字典配置、函数封装、CSV数据驱动、循环、条件断言、异常处理、JSON报告运行:python test_login.py(需同目录有test_users.csv)"""import csvimport json# ---------- 1.配置数据(字典) ----------CONFIG = { "csv_path": "test_users.csv", # 测试数据文件 "report_path": "test_report.json", # 报告输出文件}# ---------- 2.被测系统:模拟登录服务 ----------def login(username, password): """模拟登录接口:admin/123456登录成功,其余返回错误信息""" if not username or not password: return {"code": 1001, "message": "用户名或密码不能为空"} if username == "admin" and password == "123456": return {"code": 0, "message": "登录成功"} return {"code": 1002, "message": "用户名或密码错误"}# ---------- 3.测试数据驱动(CSV读取+列表遍历) ----------def load_test_data(csv_path): """从CSV加载测试数据""" test_data = [] with open(csv_path, "r", encoding="utf-8-sig") as f: # utf-8-sig兼容Excel保存的带BOM文件 reader = csv.DictReader(f) for row in reader: test_data.append(row) return test_data# ---------- 4.测试执行引擎(循环+条件断言+异常处理) ----------def run_login_test(test_cases): """执行登录测试,返回测试结果列表""" results = [] for case in test_cases: result = {"case_id": case["id"], "title": case["title"], "status": "未知"} try: resp = login(case["username"], case["password"]) # 调用被测系统 expected = case.get("expected", "success") if expected == "success": # 断言:期望登录成功 if resp["code"] == 0: result["status"] = "通过" else: result["status"] = "失败" result["error"] = f"期望登录成功,实际:{resp['message']}" elif expected == "fail": # 断言:期望登录失败 if resp["code"] != 0: result["status"] = "通过" result["actual_message"] = resp["message"] else: result["status"] = "失败" result["error"] = "期望登录失败,实际却成功了" except Exception as e: # 异常处理 result["status"] = "异常" result["error"] = f"{type(e).__name__}: {e}" results.append(result) return results# ---------- 5.生成测试报告(JSON写入) ----------def generate_report(results, output_path): """生成JSON格式的测试报告""" total = len(results) passed = sum(1 for r in results if r["status"] == "通过") failed = sum(1 for r in results if r["status"] == "失败") error = sum(1 for r in results if r["status"] == "异常") report = { "summary": { "total": total, "passed": passed, "failed": failed, "error": error, "pass_rate": f"{passed/total*100:.1f}%" if total > 0 else "0%" }, "details": results } with open(output_path, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) print("=" * 50) print(f"测试完成 | 总计:{total} | 通过:{passed} | 失败:{failed} | 异常:{error}") print(f"通过率:{report['summary']['pass_rate']}") print("=" * 50)# ---------- 6.主入口 ----------if __name__ == "__main__": test_cases = load_test_data(CONFIG["csv_path"]) if not test_cases: print("没有测试数据,请检查test_users.csv") else: results = run_login_test(test_cases) generate_report(results, CONFIG["report_path"])
test_users.csv 内容(与脚本放在同一目录):
id,title,username,password,expectedTC001,正常登录,admin,123456,successTC002,密码错误,admin,999999,failTC003,空密码,admin,,fail
运行输出(python test_login.py):
==================================================测试完成 | 总计:3 | 通过:3 | 失败:0 | 异常:0通过率:100.0%==================================================
运行后会在同目录生成test_report.json,包含统计摘要和每个用例的详细结果。把这3条用例的expected改成相反值再跑一次,你就能看到失败分支和50%以下的通过率。试着改改数据,理解断言是如何生效的。
写在最后
这篇文章以够用为原则,把测试工作中常用的Python知识点串了一遍。这些不是全部,但足够让你看懂和编写绝大部分自动化测试脚本了。
用一句话总结Python的学习思路:先运行起来,再逐步优化。 不需要先看完一整本教程才开始写测试。把本文的每个代码块复制运行一遍,然后尝试改写参数、修改逻辑,慢慢就熟练了。
下一步,我们将正式进入Selenium WebDriver的世界,用Python驱动浏览器,开始真正意义上的自动化测试。
本文为《软件测试从入门到精通》系列第29篇,本系列持续更新中。