尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python类与模块:自动化测试与接口测试的工程化基石

Python类与模块:自动化测试与接口测试的工程化基石 第四课是这套软件测试入门到精通系列教程里最关键的一次转折前几课还在讲变量、分支、循环这些单打独斗的语法从这节课开始我们进入 Python 工程化的核心——类与模块。为什么这一课对所有想转自动化测试、接口测试的零基础同学特别重要因为你只要打开任何一家公司的测试框架第一眼看到的必然是class TestLogin、from common.api_client import APIClient这类代码。看不懂类和模块你就只能复制粘贴别人写好的用例模板出了问题根本不知道去哪排查。这节课我会拆成三块来讲先讲类的定义、对象创建和self的本质再讲模块的导入机制与if __name__ __main__的底层逻辑最后用一个登录接口测试的实战案例把类和模块串成一套最小可用的自动化测试项目。学完之后你至少能独立读懂 pytest 测试类、PO 模式页面对象类并且能自己动手搭建一套测试项目的目录结构。1. 本课学习地图类与模块到底解决什么问题很多零基础同学学 Python 时最迷惑的一点是我明明用函数就能写测试脚本为什么还要学类函数把一段逻辑封装起来类把数据 操作数据的方法封装起来模块则把相关的类、函数、变量继续归档。放在软件测试里区别非常明显对比维度只用函数使用类 模块测试数据组织靠全局变量或函数传参类的属性天然承载数据公共操作复用复制粘贴函数调用继承基类复用全部能力命名空间大量同名函数互相覆盖类名 方法名双重隔离项目结构一个脚本堆到底按模块拆分多人协作排查问题靠人肉搜索按模块、类、方法逐层定位pytest 组织fixture 到处散落测试类内聚、conftest 统一管理本课核心目标可以浓缩成四句话能写出规范的测试类交给 pytest 自动收集执行。能设计一个 API 封装类把接口请求和断言分开。能理解 Python 模块导入机制解决ModuleNotFoundError。能用包结构搭建一套可维护的测试工程而不是把所有代码塞进一个.py文件。下面进入正题。顺序是先会写类再会用类最后把类装进模块。2. 类和对象把测试数据变成结构体2.1 为什么测试数据需要一个容器先看一个最常见的测试场景登录接口需要用户名、密码、期望状态码、期望提示信息。如果只用变量你会写出这种东西username admin password 123456 expected_code 200 expected_msg 登录成功这个写法的问题是如果系统里有 10 个测试用户你得写 40 个变量如果要传给函数得按顺序传参稍不留神就把password和username传反。类在这里的作用是把一组相关的数据打包成一个整体不需要关心内部每个字段的顺序。class LoginUser: def __init__(self, username, password, expected_code, expected_msg): self.username username self.password password self.expected_code expected_code self.expected_msg expected_msg user1 LoginUser(admin, 123456, 200, 登录成功) user2 LoginUser(tester, abc123, 200, 登录成功) user3 LoginUser(locker, wrong_pwd, 401, 用户名或密码错误) print(user1.username) # admin print(user3.expected_msg) # 用户名或密码错误这就是类的第一个价值它是测试数据的结构体。你不再面向一堆散落的变量编程而是面向一个个有名字的测试对象编程。2.2 类的定义与__init__构造函数所有 Python 类都通过class 类名:定义类名习惯使用大驼峰命名。__init__是构造函数在创建对象时自动执行。它的作用是初始化对象的状态也就是给对象填上出生自带的数据。class TestCaseInfo: def __init__(self, case_id, title, module, preconditions): self.case_id case_id self.title title self.module module self.preconditions preconditions case TestCaseInfo(TC001, 验证正确用户名密码可以登录, 登录模块, 系统已初始化)注意__init__的第一个参数永远是self。self不是 Python 关键字它只是一个约定俗成的名字代表当前正在创建的对象自己。当你写case.title时Python 会自动把case当作self传进去。2.3 实例属性与类属性继续用测试场景说明这两种属性的区别。class TestConfig: # 类属性所有对象共享 base_url https://api.example.com timeout 5 def __init__(self, env): # 实例属性每个对象独立 self.env env config_dev TestConfig(dev) config_prod TestConfig(prod) print(config_dev.base_url) # https://api.example.com print(config_prod.base_url) # https://api.example.com类属性相当于所有对象共用的默认配置实例属性相当于每个对象各自的环境变量。2.4 常见的面试考察点软件测试面试题里经常出现一个对比cls、self、实例方法、类方法、静态方法。这里先看代码class TestDemo: version 1.0 def __init__(self, name): self.name name def instance_method(self): # 实例方法能访问实例属性也能访问类属性 return f实例方法{self.name}版本 {self.version} classmethod def class_method(cls): # 类方法只能访问类属性不能访问实例属性 return f类方法版本 {cls.version} staticmethod def static_method(msg): # 静态方法与类和实例都没有绑定关系 return f静态方法{msg} obj TestDemo(登录用例) print(obj.instance_method()) print(TestDemo.class_method()) print(TestDemo.static_method(不访问任何对象属性))在测试框架中最常用的是实例方法因为实例方法可以同时拿到当前测试对象的数据self.name和公共配置self.version。类方法和静态方法一般用于工具类的封装后面实战部分会遇到。3. 面向对象三大特性在自动化测试中的应用3.1 封装把接口请求细节藏起来接口测试最忌讳每个用例都从头写一遍requests.post()。利用类的封装特性可以把 HTTP 请求统一封装到一个ApiClient类里调用方只关心client.post()返回什么不关心底层怎么发请求。import requests class ApiClient: def __init__(self, base_url, tokenNone): self.base_url base_url self.headers {Authorization: fBearer {token} if token else } def post(self, path, dataNone, jsonNone): url self.base_url path response requests.post(url, datadata, jsonjson, headersself.headers, timeout10) return response def get(self, path, paramsNone): url self.base_url path response requests.get(url, paramsparams, headersself.headers, timeout10) return response这样一来具体的测试用例完全不用关心requests库的细节只管调用api.post(/login, jsonpayload)。排查问题时的范围也被压缩了请求层出错看ApiClient业务层出错看用例本身。3.2 继承让测试用例共享公共逻辑当你有多个测试类都需要登录后拿 token这个前置操作时不需要在每个类里重复写一遍而是抽一个BaseTest基类class BaseTest: def setup(self): self.client ApiClient(https://api.example.com) self.token self.login_and_get_token() def login_and_get_token(self): response self.client.post(/login, json{username: admin, password: 123456}) return response.json().get(token) class TestLogin(BaseTest): def test_login_success(self): assert self.token is not None class TestUserInfo(BaseTest): def test_get_user_info(self): response self.client.get(/user/info) assert response.status_code 200TestLogin和TestUserInfo都继承了BaseTest各自不需要再写setup里的重复逻辑。这就是继承在测试项目里的最大价值把公共前置步骤上浮到基类。3.3 多态不同测试数据源走同一个接口如果你需要支持从 JSON 文件、Excel 表格、YAML 配置中读取测试数据多态可以让调用方用完全一致的方式处理不同的数据源class DataReader: def read(self): raise NotImplementedError class JsonReader(DataReader): def __init__(self, file_path): self.file_path file_path def read(self): import json with open(self.file_path, r, encodingutf-8) as f: return json.load(f) class ExcelReader(DataReader): def __init__(self, file_path): self.file_path file_path def read(self): # 实际项目中用 openpyxl 或 pandas 实现 return [{username: admin, password: 123456}] def load_test_cases(reader: DataReader): return reader.read() json_cases load_test_cases(JsonReader(data/test_data.json)) excel_cases load_test_cases(ExcelReader(data/test_data.xlsx))调用方只依赖DataReader.read()这个统一入口具体是 JSON 还是 Excel由传入的对象决定。这段代码在面试中经常作为请简述多态在测试框架中的应用的答案模板。4. 特殊方法让断言和日志更顺手Python 类里还有一批以双下划线开头和结尾的特殊方法在测试场景中有三个非常实用。4.1__str__与__repr__让测试对象可以友好打印如果你直接打印一个测试数据对象看到的是__main__.LoginUser object at 0x...完全不可读。实现__str__后打印结果变得直观class LoginUser: def __init__(self, username, password, expected_code, expected_msg): self.username username self.password password self.expected_code expected_code self.expected_msg expected_msg def __str__(self): return fLoginUser({self.username}, {self.expected_msg}) __repr__ __str__ user LoginUser(admin, 123456, 200, 登录成功) print(user) # LoginUser(admin, 登录成功)在 pytest 的断言失败信息里repr输出会直接出现在日志中能帮助你更快定位是哪条测试数据出了问题。4.2__eq__自定义对象比较逻辑断言assert resp1 resp2时如果两边都是自定义类对象Python 默认比较的是内存地址结果永远为 False。通过__eq__可以定义两个对象相等时应该比较哪些字段class ApiResponse: def __init__(self, status_code, body): self.status_code status_code self.body body def __eq__(self, other): if not isinstance(other, ApiResponse): return False return self.status_code other.status_code and self.body other.body resp1 ApiResponse(200, {message: success}) resp2 ApiResponse(200, {message: success}) print(resp1 resp2) # True4.3__call__让测试对象可以像函数一样调用如果在自动化测试中要描述一个可重复执行的检查动作可以让类的实例拥有函数一样的调用方式class StatusCodeChecker: def __init__(self, expected_code): self.expected_code expected_code def __call__(self, response): assert response.status_code self.expected_code, \ f期望状态码 {self.expected_code}实际 {response.status_code} check_200 StatusCodeChecker(200) check_200(response_obj)这种写法在复杂的断言链路中可以让代码非常简洁不过刚入门时不用强行使用了解即可。5. 模块机制拆分公共逻辑的正确姿势5.1 模块是什么模块就是一个.py文件。当你把class ApiClient写进api_client.py时api_client就是一个模块。其他文件可以通过import api_client或from api_client import ApiClient使用它。先看一个最小示例# 文件config.py BASE_URL https://api.example.com TIMEOUT 5 # 文件test_login.py import config def test_login(): print(config.BASE_URL)模块的作用有两个层面语义层面把功能分组工程层面把命名空间隔离。如果没有模块隔离你很难在一个大项目里同时使用两个都叫login的函数。5.2if __name__ __main__的底层逻辑模块在被导入时Python 解释器会把__name__变量设为模块名当模块作为主程序直接运行时则把__name__设为字符串__main__。所以下面这段代码是测试工程里的标准写法# 文件api_client.py import requests class ApiClient: pass if __name__ __main__: # 只有直接运行 python api_client.py 时才执行 client ApiClient(https://api.example.com) response client.get(/health) print(response.status_code)这样做的好处是test_tools.py被test_login.py导入时不会自动执行测试代码只有主动运行python api_client.py时才会执行内部的自测逻辑。5.3 模块的搜索路径与导入方式Python 导入模块时会依次搜索当前脚本所在目录。PYTHONPATH环境变量中指定的目录。Python 安装目录下的site-packages等标准库路径。如果找不到模块会抛出ModuleNotFoundError。排查方式就是打印sys.pathimport sys for path in sys.path: print(path)导入方式选型# 方式一导入整个模块 import config print(config.BASE_URL) # 方式二从模块导入具体对象 from config import BASE_URL print(BASE_URL) # 方式三导入多个对象 from config import BASE_URL, TIMEOUT # 方式四起别名解决命名冲突 import requests as http_client注意不要使用from module import *这种导入方式。它会污染当前命名空间让代码里出现大量不知道来源的变量在测试工程里非常不利于排查问题。5.4 常用内置模块在测试中的典型用法Python 标准库里已经有很多直接支撑测试工作的模块。下面是面试和实际项目里出现频率最高的几个import os import json import logging from pathlib import Path from datetime import datetime # os拼接路径、读取环境变量 project_dir os.path.dirname(os.path.abspath(__file__)) report_dir os.path.join(project_dir, report) # pathlib更现代的路径操作 project_path Path(__file__).resolve().parent report_path project_path / report # json接口请求与响应转换 payload {username: admin, password: 123456} json_str json.dumps(payload, ensure_asciiFalse) payload_dict json.loads(json_str) # datetime生成时间戳命名的测试报告 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) # logging给测试代码加日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logging.info(开始执行登录接口测试)内置模块不需要额外安装是测试脚本中最常用的工具集。随着课程深入后面还会接触到subprocess、threading、unittest.mock等模块在测试中的应用。6. 包与测试项目目录从零搭一套可维护的框架6.1 包和普通目录的区别包就是一个包含__init__.py文件的目录。__init__.py告诉 Python这个目录是一个可以导入的包而不是普通的文件夹。一个典型的自动化测试项目结构如下auto_test/ ├── config/ │ ├── __init__.py │ └── setting.py ├── common/ │ ├── __init__.py │ ├── api_client.py │ ├── data_reader.py │ └── logger.py ├── data/ │ └── login_data.json ├── testcase/ │ ├── __init__.py │ ├── conftest.py │ └── test_login.py ├── report/ ├── logs/ └── main.py如果目录里没有__init__.pyPython 3 在某些情况下仍能通过命名空间包导入但为了让代码在各种环境下行为一致强烈建议每个包目录都保留这个文件。6.2 不同层级之间的导入写法在testcase/test_login.py里要导入common/api_client.py中的ApiClient类正确写法是from common.api_client import ApiClient from config.setting import BASE_URL注意要以common.api_client方式导入运行命令必须从项目根目录auto_test下执行python -m pytest testcase -v6.3conftest.py的模块级共享pytest 的conftest.py是一种特殊的模块它存放 fixture可以让同一目录及子目录下的多个测试模块共用。import pytest pytest.fixture() def api_client(): from common.api_client import ApiClient return ApiClient(https://api.example.com) pytest.fixture() def login_token(api_client): response api_client.post(/login, json{username: admin, password: 123456}) return response.json().get(token)测试模块可以直接把api_client或login_token当作参数传入def test_user_info(login_token): assert login_token is not None7. 实操用类与模块完成一个登录接口测试现在把前几节的知识串起来完成一个真实的接口测试场景。假设有一个登录接口地址POST https://api.example.com/login请求体{username: admin, password: 123456}成功响应状态码 200返回{token: xxx}失败响应状态码 401返回{message: 用户名或密码错误}7.1 第一步封装 API Client 类创建common/api_client.pyimport requests class ApiClient: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout self.session requests.Session() def post(self, path, jsonNone, dataNone): url self.base_url path response self.session.post(url, jsonjson, datadata, timeoutself.timeout) return response def get(self, path, paramsNone): url self.base_url path response self.session.get(url, paramsparams, timeoutself.timeout) return response def close(self): self.session.close() if __name__ __main__: client ApiClient(https://api.example.com) resp client.post(/login, json{username: admin, password: 123456}) print(resp.status_code, resp.json()) client.close()7.2 第二步使用 pytest 测试类组织用例创建testcase/test_login.pyimport pytest from common.api_client import ApiClient class TestLogin: pytest.fixture(autouseTrue) def setup(self): self.client ApiClient(https://api.example.com) yield self.client.close() def test_login_success(self): payload {username: admin, password: 123456} response self.client.post(/login, jsonpayload) assert response.status_code 200 assert response.json()[token] def test_login_wrong_password(self): payload {username: admin, password: wrong} response self.client.post(/login, jsonpayload) assert response.status_code 401 assert response.json()[message] 用户名或密码错误这里的setup方法使用了 pytest 的 fixture 机制在每个测试方法执行前后自动调用。注意文件命名必须以test_开头测试类必须以Test开头pytest 才能自动收集。7.3 第三步把测试数据独立成模块直接写在用例里的测试数据属于硬编码。更好的方式是放到data/login_data.json[ { username: admin, password: 123456, expected_code: 200, expected_message: 登录成功 }, { username: admin, password: wrong, expected_code: 401, expected_message: 用户名或密码错误 } ]在common/data_reader.py中封装一个读取模块import json class JsonDataReader: staticmethod def read(file_path): with open(file_path, r, encodingutf-8) as f: return json.load(f)改造后的测试类import pytest from common.api_client import ApiClient from common.data_reader import JsonDataReader class TestLogin: pytest.fixture(autouseTrue) def setup(self): self.client ApiClient(https://api.example.com) yield self.client.close() pytest.mark.parametrize(case, JsonDataReader.read(data/login_data.json)) def test_login_with_data(self, case): payload {username: case[username], password: case[password]} response self.client.post(/login, jsonpayload) assert response.status_code case[expected_code]这个版本已经具备数据驱动的雏形用例逻辑不变新增测试场景只需要在 JSON 文件里加一组数据。7.4 第四步统一入口模块创建项目根目录下的main.py让整个测试可以一键执行import pytest import os if __name__ __main__: report_dir report os.makedirs(report_dir, exist_okTrue) pytest.main([ testcase, -v, --htmlreport/report.html, --self-contained-html ])运行方式python main.py到这里一条完整的链路已经闭环ApiClient类负责请求TestLogin类负责测试组织JsonDataReader模块负责数据读取main.py负责统一调度。8. 常见问题与排查类与模块相关的报错占了测试新手排错的三分之一以上。下面是最高频的问题清单问题现象可能原因排查方式解决方案ModuleNotFoundError: No module named common运行目录不在项目根目录打印sys.path查看搜索路径回到根目录运行或通过sys.path.insert(0, project_dir)临时解决pytest 收集不到测试用例测试文件没有以test_开头测试类没有以Test开头用pytest --collect-only检查收集结果按 pytest 命名规范重命名import导入了同名的另一个文件模块名和内置模块或第三方库冲突打印被导入模块的__file__属性修改模块名不叫requests.py、json.py这类名字__init__方法没有self参数忘记写self查看报错提示的构造函数签名构造函数第一个参数写self修改一个对象的属性后其他对象也被改了把可变对象放在了类属性位置在__init__里用self创建实例属性默认列表、字典等对象写入__init__循环导入a.py导入b.pyb.py又导入a.py模块之间依赖关系形成闭环查看报错中的 ImportError 栈把公共代码下沉到更低层模块或延迟导入直接运行模块时输出了测试代码没有使用if __name__ __main__检查模块底部代码缩进把自测逻辑放进if __name__ __main__:代码块AttributeError / TestLogin object has no attribute clientfixture 或 setup 方法执行顺序异常添加日志确认 setup 是否执行检查 fixture 作用域确认类内部 setup 是否被正确识别两个模块里出现同名函数后导入的覆盖了先导入的使用了from module import *检查是否还有*导入使用明确的名称或模块级导入排查建议遇到异常先看最后一行异常类型再往上翻两行看具体代码不要凭记忆猜测原因。9. 测试框架中的类与模块最佳实践9.1 命名规范对象规范示例类名大驼峰命名TestLogin、ApiClient、JsonDataReader方法名小写字母加下划线login_and_get_token、read_test_data模块名小写字母加下划线api_client.py、data_reader.py测试文件以test_开头test_login.py、test_user_info.py9.2 设计要点一个模块只做一件事。api_client.py里不要写登录用例test_login.py里不要封装 requests 请求。类应该面向业务角色设计。测试类对应测试场景页面对象类对应页面操作API 封装类对应接口能力。优先使用实例方法谨慎使用staticmethod。实例方法可以拿到对象状态排查问题时更容易通过self追踪数据流。测试用例和测试数据分离。用例只描述做什么验证数据由 JSON、YAML、Excel 等提供。公共配置放入独立模块。如config/setting.py中的BASE_URL、TIMEOUT、DB_CONFIG等避免在用例里散落硬编码数值。日志模块单独封装。便于后续接入日志文件、日志轮转和测试报告。9.3 面试中如何回答类与模块在测试中的作用可以按下面这个思路组织回答类在测试中主要解决组织问题测试类把同一功能的用例集中管理API 封装类把请求层和用例层隔离PO 模式把页面元素和测试逻辑解耦。模块解决的是工程问题公共方法、配置、数据读取逻辑分别拆成独立文件通过 import 组合使用。这样可以在团队协作中减少冲突、提高复用、方便维护。10. 总结与下一步第四课的两个关键字必须刻进记忆里类是数据和行为的组合体模块是代码的组织单元。写自动化测试时面向对象思想不是拿来装的而是真实解决重复代码、数据混乱、项目难维护的问题。pytest 测试类、PO 模式、API 封装类的底层全部建立在类和模块之上。建议你现在动手做三件事把 7.1 到 7.4 的代码敲一遍跑通一个最小接口测试项目。在data/login_data.json里加一组测试数据观察用例数是否自动增长。故意制造一个ModuleNotFoundError再通过打印sys.path排查修复把排错流程练熟。这套系列下一课会进入 pytest 断言、fixture 作用域和测试报告生成。届时你会发现今天学的类和模块会像地基一样支撑起整个进阶框架。建议把这一课的例子保存到本地练习目录后面每学一个新知识点就回来改造一次代码效果比单纯看课要好得多。
返回列表