很多刚开始学软件测试的同学,把 Python 语法“看”了一遍,遇到接口自动化或 UI 自动化项目时却仍然无从下手:不知道代码应该按什么结构组织,不知道多个文件之间怎么互相调用,更不理解为什么测试框架里的用例要写成类。其实这些问题都和“类与模块”这两个知识点有关。这一课我们围绕软件测试的真实场景,把类和模块讲透,并用一个接口测试封装的小项目把知识点串起来,让你从“会写语法”过渡到“能写测试项目”。
1. 背景与核心概念
1.1 为什么软件测试要学类与模块
软件测试的工作并不只是“点一点、录个缺陷”,在日常工作中,接口自动化、UI 自动化、测试数据准备、环境配置读取、日志封装、报告生成这些任务都需要写代码。如果只会用函数堆逻辑,测试脚本会越写越乱,最后维护成本甚至超过手工执行。
模块是一个.py文件,它把功能代码按职责拆开,比如配置单独放、请求封装单独放、用例单独放。类则是一组数据和行为的封装,对象是类的具体实例。测试场景天然适合用类来表达,因为一条测试用例通常包含前置步骤、操作步骤、结果断言、清理步骤,这些步骤如果抽象成类的方法,执行流程会非常清晰。
举个例子,你写一个登录接口的测试,不使用类是这样的:
import requests url = "http://example.com/login" data = {"username": "admin", "password": "123456"} resp = requests.post(url, json=data) if resp.status_code == 200: print("登录成功") else: print("登录失败")这段代码能跑,但如果你有几十个接口、上百条用例,全部写在一个文件里,改一个公共地址都要全局搜索替换,非常痛苦。引入模块后,可以把地址配置放到config.py;引入类后,可以把请求逻辑封装成HttpClient,把用例封装成LoginCase,各文件各司其职。
1.2 类与模块到底解决了什么问题
第一个问题是复用。模块可以被多个文件导入,类可以被多个用例继承和复用。比如所有接口都用同一个请求超时时间,这个逻辑放在http_client.py里,就只需要维护一处。
第二个问题是职责划分。测试项目里,配置、请求、断言、报告四件事最好不要混在一个函数里。模块化之后,你看到runner.py就知道它是执行入口,看到config.py就知道环境参数在那里改,不用通篇读代码才能定位。
第三个问题是可维护性。软件测试项目最大的成本不是第一版写出来,而是后续接口字段变化、环境地址变化、用例增加时的持续维护。类和模块让“变更点”尽量集中,比如接口地址变化只改配置,请求头变化只改封装类,断言变化只改用例类。这也是测试开发岗位强调代码设计能力的原因。
1.3 这一课的适用读者
本课适合以下几类读者:
- 具备 Python 基础语法、但没写过完整测试项目的测试初学者。
- 正在学习软件测试课程,准备做接口自动化或 UI 自动化项目的同学。
- 手工测试转自动化测试的工程师,希望理解测试代码如何组织。
- 面试前想梳理 Python 类、模块、面向对象知识点的求职者。
学完这一课,你会掌握:模块的导入方式、类的定义与继承、实例属性与类属性区别,并能编写一个可运行的接口测试封装小项目。
2. 环境准备与版本说明
2.1 开发环境
代码示例基于 Python。建议使用 Python 3.8 或更高版本,因为后续讲解中部分写法依赖较新的语法特性,比如 f-string 在 Python 3.6 以后才稳定,类型注解在 3.8 以后更舒服。如果你本机是 Python 3.9、3.10、3.11,完全没问题。
示例项目使用了第三方库requests,用来发送 HTTP 请求。在命令行执行安装命令:
pip install requests如果你的机器同时装有 Python2 和 Python3,可能需要用:
pip3 install requests安装完成后,可以验证一下:
python -c "import requests; print(requests.__version__)"只要不报错,说明环境可用。requests的版本没有特殊要求,使用较新的稳定版即可。
2.2 编辑器与命令行工具
我推荐使用 PyCharm 或 VS Code 作为编辑器,因为它们对模块导入、代码补全、错误提示很友好。初学者不要只靠笔记本手敲代码,虽然文本编辑器也能运行,但 IDE 能帮你提前发现很多低级错误。
命令行工具选择:
- Windows 使用 CMD 或 PowerShell。
- macOS / Linux 使用 Terminal。
在运行示例项目时,请先cd到项目根目录,再执行 Python 命令,这样才能保证模块导入路径正确。
2.3 示例项目结构
本文实战部分会创建一个名为my_api_test的测试项目,目录结构如下:
my_api_test/ ├── config.py ├── http_client.py ├── test_case.py └── runner.py每个文件职责:
config.py:存放接口地址、超时时间、请求头等配置。http_client.py:封装请求客户端类。test_case.py:定义测试用例基类和具体用例类。runner.py:程序入口,循环执行测试用例。
这个结构很小,但已经符合“配置、请求、用例、执行”四层分离的思想。后面如果项目变大,还可以继续拆出utils、report等目录。
3. 核心语法拆解:类与模块
3.1 模块的导入方式
模块本质上就是一个.py文件。导入模块后,你就能使用该文件中定义的函数、类和变量。常见导入方式有几种。
先建一个示例模块:
# 文件路径:mymodule.py def say_hello(name): return f"Hello, {name}" VERSION = "1.0" class Tool: def show(self): print("This is a tool.")在同一目录下,新建一个main.py来导入:
# 文件路径:main.py import mymodule print(mymodule.VERSION) print(mymodule.say_hello("Tester")) tool = mymodule.Tool() tool.show()使用import mymodule时,调用模块里的函数或类,必须带上模块名前缀,例如mymodule.say_hello。这种方式的优点是命名空间清晰,不容易冲突。
第二种方式是from ... import ...:
from mymodule import say_hello, Tool print(say_hello("Alice")) t = Tool() t.show()这种方式写起来简短,但如果模块里存在同名函数,可能会被覆盖。所以建议优先使用import完整导入,或者明确导入多个名称,避免from mymodule import *这种一次性导入所有内容的方式,因为它会污染当前命名空间,也不利于代码追踪。
3.2 类的定义与实例化
类是用来描述一类对象的模板。定义类时,class关键字后面跟类名,类名习惯使用大驼峰命名法。
class Car: def __init__(self, brand, color): self.brand = brand self.color = color def run(self): print(f"{self.color}的{self.brand}正在行驶")__init__是构造函数,在创建对象时自动调用。第一个参数永远是self,它指向当前实例。self.brand表示把传入的brand保存到当前实例上。
创建对象的方式是“类名加括号”,并传入构造参数:
my_car = Car("Tesla", "白色") my_car.run()在软件测试中,我们经常把“请求客户端”封装成类,构造参数就是base_url、timeout等环境信息。实例化后,每个客户端对象都保存了自己独立的配置,互不干扰。
3.3 继承与多态
继承是面向对象三大特性之一。子类可以继承父类的方法和属性,也可以重写父类方法。
以测试用例为例:
class BaseCase: def setup(self): print("准备数据") def execute(self): raise NotImplementedError("子类必须实现execute") def teardown(self): print("清理数据") class LoginCase(BaseCase): def execute(self): print("执行登录用例")这里LoginCase继承了BaseCase,它拥有了setup和teardown方法,同时重写了execute。子类可以不关心父类内部实现,只需要实现自己独有的业务逻辑。
多态在测试框架中特别常见:所有用例类都继承同一个基类,执行器遍历用例列表时,统一调用run()方法,而不同用例的execute()会执行不同的测试逻辑。这就是“一棵继承树,统一调用入口”的思想。
3.4 实例属性与类属性
初学者经常混淆类属性和实例属性。先看下面的例子:
class Counter: total = 0 def __init__(self): self.current = 0total写在class体内,是类属性,所有实例共享。current在__init__中用self.current赋值,是实例属性,每个对象各自独立。
测试一下:
a = Counter() b = Counter() a.total = 9 print(a.total) # 9 print(b.total) # 0 print(Counter.total) # 0这里有一个坑:a.total = 9并不会修改类属性,而是给实例a创建了一个新的实例属性total,把类属性挡住了。如果要修改类属性,应该操作类本身:
Counter.total = 9 print(b.total) # 9在测试项目中,不建议滥用类属性来保存测试结果这类可变状态,因为多个实例共享同一个变量,容易互相影响。如果需要共享配置,可以把配置放在常量模块中,而不是用类属性保存运行过程中的数据。
3.5 模块与类的常见误区
第一个误区是文件命名不规范。比如把文件命名为class.py、test.py、json.py,会和 Python 关键字或标准库模块重名,导入时可能产生难以排查的问题。建议文件名使用小写字母、下划线,并且避开内置模块名。
第二个误区是在被导入的模块中直接写执行代码。比如:
# config.py BASE_URL = "http://example.com" print("config 模块被导入了") requests.get(BASE_URL)如果config.py被其他模块导入,Python 会执行整个文件,打印和请求都会发生。测试项目的配置模块应该只定义变量和函数,把“执行动作”放到main入口或if __name__ == "__main__":中。
第三个误区是搞不清楚import路径。当你执行python runner.py时,Python 会在当前目录搜索模块;当项目复杂、存在多层包的时候,需要使用相对导入或设置PYTHONPATH。初学阶段,建议保持项目结构简单,运行命令时cd到项目根目录,这是最稳妥的方案。
4. 实战案例:用类与模块封装接口测试小项目
4.1 项目需求分析与功能拆分
现在我们把知识点用在真实场景中。假设要对一个接口进行冒烟测试,接口地址需要统一配置,请求需要统一处理超时,用例需要统一管理前置、执行、清理步骤。
功能拆分如下:
- 配置模块:保存
base_url、timeout、headers。 - 请求客户端类:封装 GET 和 POST 方法,统一捕获网络异常。
- 测试用例基类:定义
setup、execute、teardown、run四个方法。 - 具体用例类:分别测试 GET 接口和 POST 接口。
- 运行入口:遍历所有用例并执行。
这种设计与实际接口自动化框架具备相同的骨架,只是更精简。理解它之后,再看 pytest、unittest 等框架会轻松很多。
4.2 创建项目目录结构
在任意目录下创建一个文件夹my_api_test,目录内部结构如下:
my_api_test/ ├── config.py ├── http_client.py ├── test_case.py └── runner.py本文代码中接口使用https://httpbin.org作为示例。httpbin是一个常用于接口调试和测试的公共服务,如果网络环境无法访问,请把base_url替换为你本地可用的接口地址。
4.3 编写配置模块 config.py
配置模块负责集中管理所有可变参数。
# 文件路径:my_api_test/config.py CONFIG = { "base_url": "https://httpbin.org", "timeout": 10, "headers": { "Content-Type": "application/json" } }这里使用字典保存配置,好处是结构清晰,未来可以很方便地读取 JSON 或 YAML 配置文件。如果项目里多个模块都要导入该配置,只需要from config import CONFIG就可以了。
为什么要把配置单独放一个模块?因为测试环境、预发环境、生产环境的接口地址往往不同,如果代码里到处写死接口地址,环境切换时就要逐文件修改。统一放进配置模块,切换环境只需改一处。
4.4 封装 HTTP 请求客户端类 http_client.py
在http_client.py中,我们定义一个HttpClient类,把requests的调用细节封装起来。
# 文件路径:my_api_test/http_client.py import requests from config import CONFIG class HttpClient: def __init__(self, base_url=CONFIG["base_url"], timeout=CONFIG["timeout"]): self.base_url = base_url self.timeout = timeout self.headers = CONFIG["headers"] def get(self, path, params=None): url = self.base_url + path try: response = requests.get( url, params=params, headers=self.headers, timeout=self.timeout ) response.raise_for_status() return response except requests.RequestException as e: print(f"GET 请求失败: {e}") return None def post(self, path, data=None): url = self.base_url + path try: response = requests.post( url, json=data, headers=self.headers, timeout=self.timeout ) response.raise_for_status() return response except requests.RequestException as e: print(f"POST 请求失败: {e}") return None这段代码有几个细节值得说明。
第一,__init__方法中给base_url和timeout设置了默认值,默认值来自CONFIG。如果未来要针对某个用例修改超时时间,可以单独传参,而不影响其它用例。
第二,get和post方法内部使用try...except捕获requests.RequestException,这样网络超时、域名解析失败、连接拒绝等异常不会直接让整个测试程序中断,而是返回None,由调用方判断结果。
第三,response.raise_for_status()的作用是:如果接口返回 4xx 或 5xx 状态码,主动抛出异常。这样测试用例可以快速感知异常状态,而不是拿到一个错误响应后继续断言。
4.5 编写测试用例基类 test_case.py
测试用例基类负责控制用例的执行流程。
# 文件路径:my_api_test/test_case.py from http_client import HttpClient class BaseCase: def setup(self): self.client = HttpClient() def execute(self): raise NotImplementedError("子类必须实现 execute 方法") def teardown(self): print("执行测试数据清理") def run(self): self.setup() try: result = self.execute() except AssertionError as e: print(f"断言失败: {e}") result = False except Exception as e: print(f"用例异常: {e}") result = False finally: self.teardown() return result class GetCase(BaseCase): def execute(self): response = self.client.get("/get", params={"page": 1}) if response is None: return False assert response.status_code == 200, "状态码不是 200" data = response.json() assert data.get("args", {}).get("page") == "1", "返回参数不正确" print("GetCase 断言通过") return True class PostCase(BaseCase): def execute(self): response = self.client.post("/post", data={"name": "tester"}) if response is None: return False assert response.status_code == 200, "状态码不是 200" data = response.json() assert data.get("json", {}).get("name") == "tester", "返回 JSON 不正确" print("PostCase 断言通过") return TrueBaseCase是整个用例体系的骨架。run方法定义了标准流程:
- 调用
setup准备数据。 - 调用
execute执行测试步骤。 - 捕获断言失败和未知异常。
- 在
finally中调用teardown清理数据。
这样设计的好处是:无论用例是否通过,清理步骤都会执行。AssertionError单独捕获是因为断言失败意味着功能不符合预期,但仍然属于正常测试结果,不应该让执行器误认为程序崩溃。
GetCase和PostCase分别继承BaseCase。它们只需要重写execute,不需要重写setup和teardown。如果某个用例需要特殊的前置条件,比如先创建数据,也可以在子类中重写setup,然后调用super().setup()。
4.6 编写运行入口 runner.py
运行入口模块负责把所有用例实例化并执行。
# 文件路径:my_api_test/runner.py from test_case import GetCase, PostCase def main(): cases = [ GetCase(), PostCase() ] for case in cases: result = case.run() print(f"用例结果: {result}") if __name__ == "__main__": main()这里的关键是if __name__ == "__main__":。它的意思是:只有当直接运行runner.py时,才会调用main();如果runner.py被其它模块导入,不会执行main。这个写法是 Python 工程中的标准做法,可以避免被导入时产生副作用。
4.7 运行与验证结果
在命令行中进入项目根目录:
cd my_api_test python runner.py如果网络正常,httpbin服务可访问,输出可能类似下面这样:
GetCase 断言通过 执行测试数据清理 用例结果: True PostCase 断言通过 执行测试数据清理 用例结果: True如果网络不通或者接口地址变了,HttpClient会打印请求失败信息,用例返回False,但程序不会崩溃。这样你就能快速区分“接口环境问题”和“断言逻辑问题”。
需要特别注意:httpbin返回的具体 JSON 结构可能会随着服务端版本变化,所以如果你的环境无法访问httpbin.org,请把接口地址替换成本地可用的服务,并调整断言字段。断言的目的不是死记接口返回,而是验证“实际结果是否等于预期”。
5. 常见问题与排查思路
5.1 ModuleNotFoundError:找不到模块
错误现象:
ModuleNotFoundError: No module named 'http_client'常见原因:
- 执行命令时不在项目根目录。
- 文件名拼写错误或大小写不一致。
- 项目结构跨多层目录,没有正确设置包导入路径。
排查步骤:
- 用
pwd或cd查看当前目录,确认自己在my_api_test下。 - 检查
import http_client和文件命名是否完全一致。 - 如果项目有多层目录,确认目录下有
__init__.py文件(Python 3 中命名空间包有时不需要,但建议保留)。
解决方案很简单:要么把当前目录加入模块搜索路径,要么把运行命令放到根目录。初学阶段直接使用根目录运行最方便。
5.2 ImportError:cannot import name
错误现象:
ImportError: cannot import name 'HttpClient' from 'http_client'常见原因:
- 类名拼写错误。
- 类定义前有语法错误导致导入失败。
- 两个模块存在循环导入。
排查步骤:
- 查看
http_client.py中是否确实定义了HttpClient类。 - 查看是否有拼写大小写问题,Python 类名是区分大小写的。
- 检查
http_client.py是否导入了test_case.py,而test_case.py又导入了http_client.py,形成循环导入。遇到循环导入时,把其中一个公共逻辑拆到新模块,或者把import移到函数内部。
循环导入是 Python 新手最容易遇到的坑。比如a.py第一行导入b,b.py第一行又导入a,启动时就会报错。解决办法是避免顶层循环依赖,尽量让模块依赖关系呈单向结构。
5.3 类变量与实例变量混淆
错误现象:
class Demo: data = [] d1 = Demo() d2 = Demo() d1.data.append("hello") print(d2.data) # ['hello']如果期望d2.data仍是空列表,说明把类属性误用成了实例属性。类的可变属性会被所有实例共享,这往往不是测试代码想要的效果。
解决方案:在__init__中使用self.data = [],让每个实例拥有独立的列表。
class Demo: def __init__(self): self.data = []这个坑在测试用例中容易造成“用例 A 改了共享数据,用例 B 被影响”的诡异问题。遇到这种情况,检查属性定义位置即可。
5.4 代码运行后没有输出
这种现象通常是因为没有进入if __name__ == "__main__":分支,或者运行了错误的文件。比如直接把runner.py导入到别处,而不是用python runner.py执行。
排查时先看终端有没有报错。如果没有报错也没有输出,大概率是运行的文件不是预期入口,或者在文件中忘记调用main()。平时建议写一个极简的runner.py验证项目流程,再逐步增加用例。
6. 最佳实践与工程建议
6.1 模块职责要单一
每个模块只做一类事情。config.py只管配置,http_client.py只管 HTTP 请求,test_case.py只管用例封装,runner.py只管执行入口。如果将来某个模块超过几百行,或者你发现自己在一个模块里既写界面操作又写数据库连接又写断言,就应该拆分了。
模块职责单一的好处是:排查问题时,你能迅速定位文件;更换测试环境时,不需要翻遍所有代码。
6.2 类设计遵循“先简单再扩展”
类不是越多越好。初学者容易陷入“为了设计而设计”的误区,建一堆抽象类、工厂类、单例类,结果一个小项目变得复杂难懂。
建议从最小可用开始。你可以先写一个BaseCase基类,若干具体用例类就够了。等业务变得复杂,比如需要处理不同协议、不同登录态,再考虑继续抽象。
6.3 避免使用 import *
不建议写from http_client import *。它会把模块里所有不以下划线开头的名字全部导入,导致名字冲突和代码可读性下降。尽量显式导入,比如:
from http_client import HttpClient这样以后看到HttpClient时,能立刻知道它来自哪里。
6.4 测试代码也要有日志和异常捕获
实战项目里,直接用print调试是最初级的做法。真实项目中,推荐使用 Python 内置的logging模块,把日志输出到控制台和文件。这样测试执行后,可以留下痕迹,方便追溯问题。本文为了演示方便使用了print,在实际项目中,建议逐步替换为logging。
异常捕获要“收得住,看得见”。不要用空except: pass,这样会把所有问题都吞掉,导致测试结果显示通过,实际接口却已经挂了。至少打印异常信息,或者记录在日志中。
6.5 模块化服务于数据驱动和框架化
当你掌握了类与模块的基础组织方式,就可以进一步把项目往数据驱动方向演进。做法是:把测试数据从用例代码中拆出来,放到 JSON、YAML 或 Excel 文件中,用专门的数据读取模块加载数据,再通过类把公共步骤和业务数据组合起来。这也是当前主流接口自动化框架的基本思路。
搜索热词里经常出现“软件测试项目实战”“前后端分离项目实战”等,实际上很多公司的测试脚本并没有用上复杂框架,而是用本文这样的模块化结构不断迭代。能把一个小项目组织清楚,比背一百个面试八股文更能体现动手能力。
7. 总结与学习路线
7.1 本课掌握的关键点
本课从软件测试的视角,完成了类与模块的入门和实战:
- 模块是一个
.py文件,通过import或from ... import导入。 - 类是对象的模板,
__init__是构造函数,self代表当前实例。 - 子类通过继承获得父类方法,也可以重写父类方法。
- 类属性和实例属性在内存共享机制上完全不同,要谨慎使用类属性保存测试数据。
- 模块划分要符合职责单一原则,配置文件、请求封装、用例逻辑、执行入口分离。
- 通过一个接口测试封装项目,你可以把 GET、POST 请求、断言、异常捕获、测试流程串成一条线。
这些内容同时也是软件测试面试常考的基础点。面试官问“Python 中类变量和实例变量有什么区别”“模块导入失败怎么排查”“如何封装一个请求工具类”,其实都是本课已经覆盖的延伸。
7.2 下一步学习路线
学完类与模块后,你可以继续按以下路线提升:
- 学习
pytest和unittest测试框架,理解类在框架中如何被自动发现和执行。 - 学习异常处理的高级用法,比如自定义异常、日志记录、失败重试。
- 学习文件读写和数据驱动,把测试数据从代码中抽离到 JSON、YAML 文件。
- 学习接口自动化项目的分层思想,比如核心封装层、业务操作层、用例层、报告层。
- 如果目标岗位涉及 UI 自动化,可以继续学习 PO 模式,它正是建立在类和模块基础之上的设计模式。
7.3 给你的小练习
为了巩固本课内容,建议完成一个小练习:把示例项目中的runner.py改造成支持“用例按列表顺序执行,并且统计通过率”的版本。不要急着上网找现成代码,先自己尝试修改,遇到问题时再回过头看模块导入和类的实现。
改完之后,可以把自己写的项目放进简历项目描述中,并说明“使用 Python 类与模块完成接口自动化基础封装,统一处理配置读取、请求发送和断言逻辑”,这套表述比单纯写“熟悉 Python”更有说服力。写到这里,类与模块的第一阶段就通关了。接下来建议你打开编辑器,把本文的接口请求封装改造成自己的第一个测试小工具,然后继续下一课:异常处理与测试报告生成。