软件测试课程总结:3个高频面试必问实战项目复盘
看了一堆视频还是不会写项目?别慌,我踩过的坑你都会。 面试必问的自动化测试框架,光看理论根本记不住。 这篇软件测试课程总结,直接给你能跑通的代码和避坑指南。
项目目标:从脚本到框架的跃迁
很多初学者有个误区,觉得会写几条 unittest 用例就算会测试了。错得离谱。
企业级项目里,测试代码和开发代码一样,都要讲工程化。
我们的目标不是写几个孤立的函数,而是搭建一个可维护、可扩展的测试框架。
这个框架要解决三个核心痛点:
- 数据驱动:测试数据与代码分离,改数据不用改代码。
- 环境隔离:不同环境(测试、预发、生产)配置自动切换。
- 结果可视化:失败截图、日志自动归档,邮件通知一键发送。
回想一下你之前的测试脚本,是不是数据硬编码在代码里? 改个手机号,全文搜索替换,改漏一个就出 Bug。 这种写法在面试里属于“初级水平”,面试官一眼就能看出来。 我们要做的,是把测试代码当成产品来做,而不是当成一次性任务。
目录结构:规范先行,拒绝混乱
动手写代码前,先把目录结构定好。 乱糟糟的目录,是维护噩梦的根源。 以下是我推荐的标准项目结构,照着抄就行:
test_project/
├── config/
│ └── config.ini # 配置文件,管理环境参数
├── data/
│ └── test_data.csv # 测试数据,CSV或Excel格式
├── cases/
│ ├── test_login.py # 登录模块测试用例
│ └── test_order.py # 订单模块测试用例
├── common/
│ ├── base_test.py # 基类,封装公共逻辑
│ ├── utils.py # 工具类,文件操作、日志等
│ └── db_helper.py # 数据库操作封装
├── report/
│ └── index.html # 自动生成测试报告
├── logs/
│ └── test.log # 运行日志
├── screenshots/
│ └── error.png # 失败截图
├── conftest.py # Pytest 全局配置
└── run_test.py # 入口文件
这个结构有几个关键点必须注意:
config 目录:不要硬编码 URL 或账号密码。
所有环境相关的变量,全部丢进 config.ini。
这样切换环境,只需要改一个文件,不用动代码。
cases 目录:按业务模块划分文件。
登录、注册、支付,每个模块一个文件。
文件命名统一以 test_ 开头,这是 Pytest 的识别规则。
common 目录:这是框架的灵魂。 所有重复的逻辑,比如数据库连接、HTTP 请求、日志打印,全部封装在这里。 用例文件里只写业务逻辑,不写底层操作。
report 和 logs:自动生成的目录。 不要手动创建文件,让代码去生成。 每次运行测试,自动覆盖旧报告,生成新的。
核心代码实现:基类与数据驱动
接下来是干货。 我们不用复杂的 Selenium,就用最轻量的 Pytest + Requests + Allure。 这套组合拳,覆盖了 80% 的接口测试场景。
1. 配置文件管理
先看 config/config.ini:
[TEST]
base_url = http://test-api.example.com
username = test_user
password = test_pass_123
timeout = 10[PROD]
base_url = http://api.example.com
username = prod_user
password = prod_pass_123
timeout = 5
然后在 common/utils.py 里读取配置:
import configparser
import osclass Config:def __init__(self, env='TEST'):self.env = envself.config = configparser.ConfigParser()# 获取当前文件所在目录的上一级,即项目根目录self.base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))config_file = os.path.join(self.base_path, 'config', 'config.ini')self.config.read(config_file, encoding='utf-8')def get(self, key):# 动态获取当前环境的配置值return self.config.get(self.env, key)# 全局单例,避免重复创建
config = Config()
这样,任何地方想拿 base_url,直接 config.get('base_url') 就行。
想切环境?改 Config(env='PROD') 就行。
简单、直接、不易出错。
2. 基类封装:公共逻辑复用
common/base_test.py 是核心中的核心。
import requests
import pytest
import allure
import os
from datetime import datetimeclass BaseTest:def __init__(self):self.headers = {"Content-Type": "application/json","Accept": "application/json"}self.base_url = config.get('base_url')self.timeout = int(config.get('timeout'))def request(self, method, url, json_data=None, params=None):"""封装统一的 HTTP 请求方法:param method: 请求方法 GET/POST/PUT/DELETE:param url: 接口路径:param json_data: 请求体数据:param params: URL 查询参数:return: 响应对象"""full_url = f"{self.base_url}{url}"with allure.step(f"发送请求: {method} {full_url}"):try:if method.upper() == 'GET':resp = requests.get(full_url, params=params, headers=self.headers, timeout=self.timeout)elif method.upper() == 'POST':resp = requests.post(full_url, json=json_data, headers=self.headers, timeout=self.timeout)else:resp = requests.request(method.upper(), full_url, json=json_data, headers=self.headers, timeout=self.timeout)# 记录请求详情到日志print(f"[REQUEST] {method} {full_url} | Body: {json_data} | Params: {params}")print(f"[RESPONSE] Status: {resp.status_code} | Body: {resp.text[:200]}...")return respexcept Exception as e:allure.attach(str(e), name="请求异常")raisedef login(self, username, password):"""登录接口封装,返回 Token这是高频考点:接口鉴权处理"""url = "/api/v1/login"data = {"username": username, "password": password}resp = self.request("POST", url, json_data=data)# 校验登录是否成功assert resp.status_code == 200, f"登录接口状态码异常: {resp.status_code}"resp_json = resp.json()assert resp_json.get("code") == 0, f"登录失败: {resp_json.get('msg')}"token = resp_json.get("data", {}).get("token")# 将 Token 存入全局或类变量,供后续用例使用self.headers["Authorization"] = f"Bearer {token}"return token
注意这里的 assert 断言。
很多新手喜欢用 if-else 判断,然后 print 结果。
错!测试用例必须用断言。
断言失败,Pytest 会直接标记为 Fail,并记录堆栈信息。
用 print 只是打印,测试依然显示 Pass,这就是“假阳性”,是大忌。
3. 数据驱动用例
cases/test_login.py:
import pytest
import allure
from common.base_test import BaseTestclass TestLogin(BaseTest):@allure.feature("用户登录模块")@allure.story("登录功能验证")def test_login_success(self):"""测试正常登录"""# 从配置文件获取测试账号username = config.get('username')password = config.get('password')token = self.login(username, password)# 断言 Token 不为空assert token is not Noneassert len(token) > 10@pytest.mark.parametrize("username, password, expected_code", [("test_user", "wrong_pass", 400),("", "", 400),("test_user", "", 400),("", "test_pass_123", 400)])@allure.title("异常登录场景")def test_login_fail(self, username, password, expected_code):"""测试异常登录场景,数据驱动"""url = "/api/v1/login"data = {"username": username, "password": password}resp = self.request("POST", url, json_data=data)# 断言状态码符合预期assert resp.status_code == expected_coderesp_json = resp.json()assert resp_json.get("code") != 0
看这个 @pytest.mark.parametrize。
这是数据驱动的核心。
四个异常场景,一行代码搞定。
如果不用数据驱动,你得写四个方法,复制粘贴四遍。
代码冗余、维护困难、容易出错。
面试时,如果问你“如何设计异常场景测试”,
直接甩出这个 parametrize 代码。
面试官立刻就会觉得你懂工程化思维。
运行与测试:Allure 报告与日志
代码写完了,怎么跑?怎么看结果?
手动跑 pytest 命令行,结果全是文本,根本没法看。
必须上 Allure 报告。
1. 安装依赖
pip install pytest requests allure-pytest
# 安装 Allure 命令行工具(需要 JDK 环境)
# 或者使用 Docker 运行 Allure
docker run --rm -it -v $(pwd)/report:/results:ro -p 8080:8080 qameta/allure serve /results
2. 运行测试
在项目根目录执行:
# 生成测试数据到 report 目录
pytest cases/ --alluredir=report --clean-alluredir -v
--alluredir=report 指定结果输出目录。
--clean-alluredir 每次运行前清空旧数据,避免数据污染。
-v 显示详细日志。
3. 查看报告
运行完成后,启动 Allure 服务:
allure serve report
浏览器自动打开 http://localhost:8080。
你会看到:
- 测试通过率:一眼看出健康度。
- 失败用例详情:点击失败用例,能看到完整的请求报文、响应报文、堆栈信息。
- 步骤追踪:我们代码里写的
allure.step,在这里会展示成时间线。 - 附件查看:如果有截图或日志文件,可以直接在线查看。
这个报告,就是你交付给开发的“证据”。 不是你说“我测过了,没问题”,而是“这是报告,失败率 0%,所有用例通过”。 专业度瞬间拉满。
4. 日志记录
光有 Allure 报告还不够。 接口测试经常遇到“偶现问题”,Allure 报告里可能只记录最后一次运行。 我们需要详细的日志文件。
在 common/utils.py 里添加日志配置:
import logging
import os
from datetime import datetimedef setup_logger():"""配置日志,同时输出到控制台和文件"""logger = logging.getLogger("test_logger")logger.setLevel(logging.INFO)# 避免重复添加 Handlerif logger.handlers:return logger# 文件 Handlerlog_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')if not os.path.exists(log_dir):os.makedirs(log_dir)log_file = os.path.join(log_dir, f"test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")file_handler = logging.FileHandler(log_file, encoding='utf-8')file_handler.setLevel(logging.INFO)# 控制台 Handlerconsole_handler = logging.StreamHandler()console_handler.setLevel(logging.INFO)# 设置格式formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')file_handler.setFormatter(formatter)console_handler.setFormatter(formatter)logger.addHandler(file_handler)logger.addHandler(console_handler)return logger# 全局日志对象
logger = setup_logger()
在 base_test.py 的 request 方法里,把 print 换成 logger.info:
logger.info(f"[REQUEST] {method} {full_url} | Body: {json_data}")
logger.info(f"[RESPONSE] Status: {resp.status_code} | Body: {resp.text[:200]}...")
这样,每次运行测试,都会生成一个独立的日志文件。 文件名带时间戳,不会覆盖。 出问题的时候,翻日志比看 Allure 报告更彻底。
优化扩展:从单接口到全链路
现在的框架,能跑通单个接口测试。 但实际项目中,经常需要测“链路”。 比如:登录 -> 下单 -> 支付 -> 查询订单。 这种场景,怎么设计?
1. 接口依赖处理
在 base_test.py 里,我们已经在 login 方法里把 Token 存到了 self.headers。
后续所有需要鉴权的接口,直接复用这个 headers 就行。
def create_order(self, product_id, quantity):"""创建订单,依赖登录 Token"""url = "/api/v1/orders"data = {"product_id": product_id,"quantity": quantity}# 此时 self.headers 里已经有 Token 了resp = self.request("POST", url, json_data=data)assert resp.status_code == 200return resp.json().get("data", {}).get("order_id")
2. 数据库校验
接口返回“成功”,不代表数据真的写进库了。 特别是涉及金额、库存的场景,必须查库验证。
在 common/db_helper.py 里封装数据库操作:
import pymysql
import configclass DbHelper:def __init__(self):self.connection = pymysql.connect(host='127.0.0.1',port=3306,user='test_user',password='test_pass',database='test_db',charset='utf8mb4')self.cursor = self.connection.cursor(pymysql.cursors.DictCursor)def execute_query(self, sql, params=None):"""执行查询,返回结果集"""try:self.cursor.execute(sql, params)return self.cursor.fetchall()finally:self.cursor.close()def close(self):self.connection.close()db = DbHelper()
在用例里查库验证:
def test_order_flow(self):"""完整链路测试:登录 -> 下单 -> 查库验证"""# 1. 登录token = self.login(config.get('username'), config.get('password'))# 2. 下单order_id = self.create_order(product_id=1001, quantity=2)assert order_id is not None# 3. 查库验证sql = "SELECT status, amount FROM orders WHERE order_id = %s"result = db.execute_query(sql, (order_id,))assert len(result) == 1, "订单未入库"order_data = result[0]assert order_data['status'] == 'PENDING', f"订单状态异常: {order_data['status']}"assert order_data['amount'] == 200.00, f"订单金额异常: {order_data['amount']}"# 4. 清理数据(可选)# db.execute_query("DELETE FROM orders WHERE order_id = %s", (order_id,))
这种“接口 + 数据库”的双重验证,是高级测试工程师的标配。 面试时,如果你能说出“我不仅验接口返回值,还会查库验证数据一致性”, 面试官会对你刮目相看。
3. 性能监控
在 base_test.py 的 request 方法里,加上耗时统计:
import timedef request(self, method, url, json_data=None, params=None):full_url = f"{self.base_url}{url}"start_time = time.time()with allure.step(f"发送请求: {method} {full_url}"):# ... 请求代码 ...end_time = time.time()duration = (end_time - start_time) * 1000logger.info(f"[PERF] {method} {url} 耗时: {duration:.2f}ms")# 可以设置性能阈值,超时报警if duration > 2000:logger.warning(f"接口响应超时: {url} 耗时 {duration}ms")return resp
这样,每次运行测试,日志里都会有接口耗时。 长期运行下来,你就能发现哪些接口变慢了。 性能问题,往往是从“慢”开始的。
小结:从会用框架到理解本质
到这里,一个完整的接口测试框架就搭完了。 配置管理、基类封装、数据驱动、Allure 报告、日志记录、数据库校验,全都齐了。
但我想强调一点: 框架只是工具,理解测试本质才是关键。
面试时,如果问你“为什么这么设计框架”, 不要只说“为了方便”。 要说:
- 降低维护成本:数据与代码分离,改数据不用改代码。
- 提高复用率:公共逻辑封装在基类,用例只写业务。
- 增强可追溯性:日志 + 报告,出问题能快速定位。
- 保障数据一致性:接口 + 数据库双重验证,避免假阳性。
这些才是面试官想听到的。 他们想看的,不是你会不会写代码,而是你有没有工程化思维。
软件测试这门课,很多人学到最后,还是只会点点点。 但真正的竞争力,在于你能不能用代码解决重复劳动, 能不能用框架保证测试的稳定性, 能不能用数据证明测试的有效性。
这个框架,你拿去就能用。 改改配置,接上你的项目,跑起来。 跑的过程中,会遇到各种坑。 比如数据库连接池耗尽,比如接口 Mock 不彻底,比如并发测试数据冲突。 这些坑,踩过了,才是你的经验。
还有什么是你不懂的? 比如怎么接入 CI/CD 流水线? 比如怎么做分布式压测? 比如怎么设计自动化测试覆盖率指标? 评论区留言,挨个回。 别客气,问得越细,答得越透。