ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

软件测试课程总结:3个高频面试必问实战项目复盘

软件测试课程总结:3个高频面试必问实战项目复盘 软件测试课程总结: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 流水线? 比如怎么做分布式压测? 比如怎么设计自动化测试覆盖率指标? 评论区留言,挨个回。 别客气,问得越细,答得越透。
返回列表