
软件测试课程总结: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/logindata = {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] = fBearer {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/logindata = {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 /results2. 运行测试
在项目根目录执行:
# 生成测试数据到 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, ftest_{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):创建订单,依赖登录 Tokenurl = /api/v1/ordersdata = {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 = %sresult = 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 流水线?
比如怎么做分布式压测?
比如怎么设计自动化测试覆盖率指标?
评论区留言,挨个回。
别客气,问得越细,答得越透。