基于Python的自动化测试系统设计与实践:从pytest到CI集成
2026/9/19 18:02:44 网站建设 项目流程

简介:一份基于Python语言的自动化测试系统毕业论文文档,面向专科与本科毕业生、自动化测试初学者及需要毕业设计参考的学生,可直接用于理解自动化测试从理论到落地的完整路径。内容围绕自动化测试系统的设计与实现展开,系统阐述绪论、系统设计、系统实现、系统测试与评价、结果分析及总结展望,并融入Python、Django框架、数据爬取和人脸识别等关键技术。包体为单个docx文档,压缩包大小约30KB,结构简洁,便于直接阅读和二次修改。文档中详细说明了requests库模拟HTTP请求、Scrapy框架爬取测试数据、Tesseract OCR识别图像文本以及Face_recognition库实现人脸识别等核心实现,同时对测试用例管理、测试执行、结果分析和报告生成等模块进行了设计说明,可帮助读者梳理论文框架与实现思路。目前已有261人学习,适合正在撰写自动化测试方向论文或希望了解Python测试技术实际应用的学生参考。

1. 自动化测试系统的边界:不是把手工点一遍换成脚本点一遍

手工测试真正贵的不是“点”,而是回归。上线前全量冒烟要占掉多少人天、改一次公共模块要连带验证多少个历史用例,这些成本在项目迭代到第三个月之后会迅速压过功能开发的投入。用 Python 做自动化测试系统,核心价值不在“自动执行”,而在把用例编写、执行调度、结果判定、报告生成串成一条可重复的流水线,让测试代码像业务代码一样有版本、有组织、能被复用。

这篇内容要拆的是这样一个系统的完整骨架:从四模块架构和数据库设计开始,落到 pytest + Selenium 的脚本组织方式,再讲 Django 后台、数据爬取和人脸识别模块如何接入测试流程,最后收在 Jenkins 集成和排错技巧上。适合正在用 unittest 或 pytest 写零星脚本、想把它升级成正式系统的测试开发,也适合做类似选题时需要一套可复现方案的毕业生。

2. 系统总体架构与数据库设计:先定边界,再写代码

自动化测试系统最容易犯的错误是一上来就写脚本,写到一半发现用例多了没法管理、结果没法回溯、报告没人看。论文里给出的四个模块——测试用例管理、测试执行、测试结果分析、测试报告生成——本质上是在给测试工作划边界。用例管理解决“测什么”,执行模块解决“怎么跑”,结果分析解决“跑完怎么看”,报告生成解决“怎么让别人看”。

2.1 四模块职责划分

四个模块的依赖关系是单向的。用例管理模块向下游提供用例数据和套件组织方式,执行模块读取这些数据后调度测试进程,执行过程中产生的原始结果交给分析模块做统计,分析结果再由报告模块渲染输出。模块间不能交叉调用,否则后期改一个模块会牵动整条链路。

  • 用例管理:维护用例的增删改查、标签、优先级、套件归属,支持从 Excel 或 CSV 批量导入。常见做法是用一套 Web 界面操作,数据落在数据库里,执行端通过接口拉取。
  • 测试执行:负责加载用例、初始化测试数据、按顺序或按依赖关系执行,收集通过率、失败原因、执行耗时。多线程或分布式执行在这里接入。
  • 结果分析:对执行记录做聚合统计,比如按模块统计失败率、按时间段统计稳定性、定位高频失败用例。
  • 报告生成:把统计结果渲染成 HTML 或 PDF,支持自定义模板,能自动附带失败截图和堆栈信息。

2.2 核心表结构与建表 SQL

数据库设计方面,SQLite 适合单机执行场景,部署简单、无需单独维护服务;如果系统要支持多人协作和 Web 端同时读写,换成 MySQL 或 PostgreSQL,字段设计逻辑是一致的。核心是四张表:测试套件表、测试用例表、执行记录表、报告表。

CREATE TABLE test_suite ( suite_id INTEGER PRIMARY KEY AUTOINCREMENT, suite_name VARCHAR(128) NOT NULL UNIQUE, description TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE test_case ( case_id INTEGER PRIMARY KEY AUTOINCREMENT, suite_id INTEGER NOT NULL, case_name VARCHAR(128) NOT NULL, steps TEXT, expect_result TEXT, priority INTEGER DEFAULT 3, tags VARCHAR(255), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (suite_id) REFERENCES test_suite(suite_id) ); CREATE TABLE test_execution ( exec_id INTEGER PRIMARY KEY AUTOINCREMENT, case_id INTEGER NOT NULL, status VARCHAR(16) NOT NULL, duration_ms INTEGER, error_message TEXT, screenshot TEXT, exec_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (case_id) REFERENCES test_case(case_id) ); CREATE TABLE test_report ( report_id INTEGER PRIMARY KEY AUTOINCREMENT, exec_start TIMESTAMP, exec_end TIMESTAMP, total_count INTEGER, pass_count INTEGER, fail_count INTEGER, skip_count INTEGER, report_path TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

建表逻辑里几个关键点:test_case.steps不用 JSON,用 TEXT 存纯文本或按行分隔的步骤描述,这样在 SQLite 里查询和导出都方便,真要结构化可以后续再解析;priority用整数不用字符串,1 到 5 级,排序和过滤效率更高;test_executiontest_report都带时间戳,后续做趋势分析直接按时间聚合。

关联关系上,套件与用例是一对多,用例与执行记录是一对多。在suite_idcase_id上建索引,否则执行记录表量级上去之后,按用例查历史结果会明显变慢。主键用自增整数够用,不需要 UUID,这套系统的数据量级远没到分布式 ID 的瓶颈。

3. 测试框架选型与脚本组织:pytest 是底座,PO 模式是骨架

框架选型决定了脚本的写法和维护成本。很多人纠结 unittest 还是 pytest,其实从 3 开始,pytest 已经内置了对 unittest 用例的兼容,可以直接跑,所以新项目没有理由再从 unittest 起步。

3.1 主流框架对比

框架断言风格参数化Fixture插件生态适用场景
unittest继承 TestCase 的 assert 方法无原生支持,需外部库setUp/tearDown较少老项目兼容、标准库依赖
pytest原生 assert + 重写报错内置 parametrizefixture 机制,作用域可控非常丰富新项目首选,接口/UI/单元全覆盖
Robot Framework关键字表驱动数据表驱动Suite/Test 级有,但依赖 Java 生态业务人员维护用例的场景

选 pytest 的另外三个理由:fixture 的scope参数可以控制 session/module/class/function 四个级别的复用,能省掉大量重复初始化的代码;parametrize直接解决数据驱动问题,不用自己写循环;插件生态里有pytest-htmlpytest-xdistpytest-rerunfailures,覆盖报告、并行、重试三个高频需求。

3.2 页面对象模式的落地写法

页面对象模式(Page Object Model)的核心目的是把页面元素定位和业务操作分离。业务层不出现find_element这类调用,只调用页面对象的方法,这样页面结构变了,只需要改对应页面对象。

# base_page.py from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver: webdriver.Remote): self.driver = driver self.wait = WebDriverWait(driver, timeout=10) def click(self, locator: tuple): # 显式等待元素可点击,避免网络慢导致的 NoSuchElementException self.wait.until(EC.element_to_be_clickable(locator)).click() def input_text(self, locator: tuple, text: str): element = self.wait.until(EC.visibility_of_element_located(locator)) element.clear() # 分次输入比 send_keys 更稳定,避免中文输入丢字符 if text: element.send_keys(text) def get_text(self, locator: tuple) -> str: return self.wait.until(EC.visibility_of_element_located(locator)).text
# login_page.py from selenium.webdriver.common.by import By from base_page import BasePage class LoginPage(BasePage): # 元素定位集中定义,页面结构变化只改这里 username_input = (By.ID, "username") password_input = (By.ID, "password") login_button = (By.CSS_SELECTOR, "button[type='submit']") error_msg = (By.CLASS_NAME, "error-tip") def login(self, username: str, password: str): self.input_text(self.username_input, username) self.input_text(self.password_input, password) self.click(self.login_button) def get_error_message(self) -> str: return self.get_text(self.error_msg)

BasePageclickinput_text都用了显式等待而不是time.sleep,区别在于等待条件是元素状态而不是固定时间。固定等待在 CI 机器负载高时会误报,负载低时又浪费时间;显式等待只在条件满足时继续执行,默认 10 秒超时,超时会抛TimeoutException,日志里能看到具体是哪个元素没出现。By.CSS_SELECTOR定位优先,比 XPath 稳定且快,只有 CSS 表达不了复杂层级时才用 XPath。

3.3 数据驱动与关键字驱动的差异

数据驱动适合接口测试和表单类用例,pytest 的parametrize是最轻量的实现方式。在test_login.py中,把用户名、密码、预期结果放在外部 CSV 里,用@pytest.mark.parametrize注入,新增一组数据不用改代码。

import csv import pytest from login_page import LoginPage def load_credentials(): cases = [] with open("testdata/login_data.csv", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: # 跳过被注释掉的用例,便于临时禁用 if row["enabled"].lower() == "true": cases.append((row["username"], row["password"], row["expected"])) return cases @pytest.mark.parametrize("username,password,expected", load_credentials()) def test_login_cases(username, password, expected, driver): page = LoginPage(driver) page.login(username, password) if expected == "success": assert "dashboard" in driver.current_url else: assert page.get_error_message() == expected

encoding="utf-8-sig"会去掉 BOM 头,否则 Excel 保存的 CSV 会在第一列字段名里带\ufeff,导致参数名不匹配。这里的driver是 fixture,通常放在conftest.py里配置 session 级初始化,每个测试函数接收同一个浏览器实例。

关键字驱动比数据驱动多一层抽象:把测试步骤抽象成“关键字 + 参数”,由执行引擎解释运行。比如步骤login | admin | 123456,引擎根据关键字login找到对应函数并传入两个参数。优点是业务人员可以用 Excel 写用例,缺点是 Debug 困难,报错堆栈定位不到具体代码行。对这个项目来说,pytest 的数据驱动已经覆盖大部分场景,关键字驱动适合测试团队没有编程能力的情况,不建议一上来就做。

4. Django 后端接入与数据采集、人脸识别模块实现

论文的摘要里把 Django、数据爬取、人脸识别列为关键知识点,这三块在自动化测试系统里分别对应后台服务、测试数据准备、图像验证三个环节。Django 承担用例管理和报告展示的后台接口,爬虫解决测试数据来源,人脸识别用于图像类接口的自动化断言。

4.1 Django 后台服务与接口设计

Django 作为后端服务,核心是把数据库里的用例和执行结果暴露成 REST 接口,前端界面或测试执行机通过 HTTP 调用。搭建时用 Django REST Framework,序列化器直接映射模型,不必手写 JSON 拼接。

# apps/case/serializers.py from rest_framework import serializers from .models import TestCase, TestSuite class TestCaseSerializer(serializers.ModelSerializer): # 嵌套返回套件名称,避免前端拿 ID 再查一次 suite_name = serializers.CharField(source="suite.suite_name", read_only=True) class Meta: model = TestCase fields = ["case_id", "case_name", "suite_name", "priority", "tags"]
# apps/case/views.py from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import IsAuthenticated from .models import TestCase from .serializers import TestCaseSerializer class TestCaseViewSet(ModelViewSet): queryset = TestCase.objects.select_related("suite").all() serializer_class = TestCaseSerializer permission_classes = [IsAuthenticated] def get_queryset(self): # 支持按优先级和标签过滤,让执行端可以按条件拉取用例 queryset = super().get_queryset() priority = self.request.query_params.get("priority") tag = self.request.query_params.get("tag") if priority: queryset = queryset.filter(priority=priority) if tag: queryset = queryset.filter(tags__icontains=tag) return queryset

select_related("suite")解决 N+1 查询问题:不加它会为每条用例单独查一次套件表,用例多时接口明显变慢;加了之后一次性JOIN查出。IContains对应 SQL 里的LIKE,做标签模糊过滤够用。IsAuthenticated权限类要求所有接口带 Token,避免内网测试平台被人随意改数据。

4.2 requests 与 Scrapy 在测试数据准备中的应用

自动化测试系统里,数据爬取不是业务功能,而是为了构造测试数据。比如需要批量生成不同用户名、手机号、地址的注册用例,手工造数据效率太低,从公开页面抓取再清洗是常见做法。

import requests from bs4 import BeautifulSoup def fetch_test_names(count=100): url = "https://example.com/api/names" params = {"format": "json", "count": count} resp = requests.get(url, params=params, timeout=10) # 显式校验状态码,避免页面返回错误页面时被解析成空数据 resp.raise_for_status() data = resp.json() return [item["name"] for item in data["results"]]

raise_for_status()很关键,不调用的话,接口返回 500 时resp.json()会拿到错误页面的 HTML,解析时报错信息不直观。timeout参数必须设置,否则某个请求挂住会让整个测试执行卡死。批量生成的数据存在测试库或 CSV 里,用 pytest 的parametrize加载即可。

爬虫任务量再大一点,单线程 requests 就不够了,换 Scrapy 做并发抓取。Scrapy 的Request回调机制天然适合多级页面采集,比如先抓列表页拿详情链接,再并发抓详情页字段。注意抓取频率设置DOWNLOAD_DELAY = 0.5,单位是秒,控制单请求间隔,避免对目标站点造成压力。测试数据爬取只做合法公开数据的采集,不碰登录态和隐私字段,这个边界要守住。

4.3 Tesseract OCR 与 Face_recognition 的图像断言

图像类测试是 UI 自动化的难点。验证码识别、图片中的文字校验、人脸登录模块的返回结果,这些用find_element拿不到文本内容,需要 OCR 或人脸特征比对。

import pytesseract from PIL import Image def extract_text_from_image(image_path: str) -> str: """识别截图中的文本,用于校验页面上渲染的公告或协议内容。""" with Image.open(image_path) as img: # gray 预处理能显著提高 OCR 准确率,彩色背景会干扰文字分割 gray = img.convert("L") text = pytesseract.image_to_string(gray, lang="chi_sim+eng") return "".join(text.split())

convert("L")把图像转成灰度,去掉彩色干扰;lang="chi_sim+eng"同时加载中文简体与英文语言包,语言包要用 Tesseract 的language管理命令单独安装,不是装完 Tesseract 就有。注意image_to_string返回的内容含大量换行和空格,"".join(text.split())去掉空白字符后再做断言匹配,否则经常因为换行位置不同而误判失败。

人脸识别模块用于测试带人脸登录功能的系统。face_recognition库封装了 dlib 的人脸特征提取,接口很简单,但要注意模型文件和计算资源的消耗。

import face_recognition def verify_face(image_path: str, known_face_path: str, tolerance: float = 0.6) -> bool: # 加载样本图片并提取 128 维人脸特征向量 known_image = face_recognition.load_image_file(known_face_path) known_encoding = face_recognition.face_encodings(known_image) # 有人脸才继续比对,图中无人脸直接返回 False,避免空列表索引报错 if not known_encoding: return False test_image = face_recognition.load_image_file(image_path) test_encodings = face_recognition.face_encodings(test_image) for encoding in test_encodings: # compare_faces 返回布尔列表,tolerance 越小匹配越严格 matches = face_recognition.compare_faces([known_encoding[0]], encoding, tolerance=tolerance) if any(matches): return True return False

tolerance是误判率开关,默认 0.6 对一般场景够用;安全要求高的场景调到 0.4 以下,但人脸角度和光线变化大时会导致大量漏报,自动化测试里通常先确认测试图片是正脸且光照均匀。face_recognition首次运行会加载约 150MB 的模型到内存,并发执行时不要每张图都重新加载模型,放在模块级或 fixture 的 session 作用域里复用。

5. 执行调度、CI 集成与高频排错技巧

系统开发完成后,落地效果取决于执行链路是否稳定。这一章讲命令行参数管理、并发执行、Jenkins 集成,以及三个最容易踩的坑。

5.1 命令行参数与并行执行

测试执行机通过命令行参数控制要跑的用例范围和环境,而不是改代码里的配置。用 pytest 的addoption扩展自定义参数。

# conftest.py def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="test/staging/prod") parser.addoption("--suite", action="store", default="", help="只执行指定套件") @pytest.fixture(scope="session") def env(request): return request.config.getoption("--env") @pytest.fixture(scope="session") def base_url(env): # 不同环境映射不同地址,参数从环境变量读取,避免敏感信息写死在代码里 return { "test": "http://test.example.com", "staging": os.environ.get("STAGING_URL"), }.get(env, "http://localhost:8000")

执行命令示例:

pytest -n 4 --dist=loadscope --env=staging --suite=login \ --html=report.html --self-contained-html --maxfail=5

-n 4是 pytest-xdist 的并行数,--dist=loadscope按模块分配进程,同一个测试模块里的用例不会分散到不同进程,避免共享状态的冲突。--self-contained-html把 CSS 和 JS 内嵌进 HTML 报告,单独发文件给别人看时样式不会丢。--maxfail=5在连续失败 5 条后停止,配合 CI 快速失败策略,省得等全部跑完才意识到环境挂了。

5.2 Jenkins Pipeline 集成

自动化测试要真正产生价值必须跑在 CI 里,提交代码自动触发测试任务。Jenkins Pipeline 脚本在项目根目录放Jenkinsfile,把构建、测试、报告归档串起来。

pipeline { agent any environment { PYTHON_ENV = "staging" } stages { stage('Install Dependencies') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt' } } stage('Run Tests') { steps { sh '. venv/bin/activate && pytest -n 4 --env=${PYTHON_ENV} --html=report.html --self-contained-html --junitxml=result.xml' } } stage('Archive Report') { steps { archiveArtifacts artifacts: 'report.html', allowEmptyArchive: true junit 'result.xml' } } } }

junit指令会把 pytest 生成的 JUnit XML 解析成趋势图和失败列表,在 Jenkins 界面上直接看到最近 50 次执行的通过率曲线。allowEmptyArchive: true是防止测试全部跳过时没有报告文件导致构建标记失败。虚拟环境用python3 -m venv而非系统 Python,避免跑到环境里其他安装的包版本。

5.3 高频问题排查

现象根因处理方式
脚本本地能跑,Jenkins 上报元素找不到服务器上浏览器版本与 Selenium 驱动不匹配用 Selenium Manager 自动匹配驱动版本,或固定浏览器和驱动版本组合
并行执行时用例互相影响共享了同一个测试账号或同一份测试数据每个 worker 使用独立账号,测试数据按 worker 编号隔离
OCR 识别结果总是不对截图里文字带抗锯齿或背景复杂做放大和灰度预处理,必要时先做二值化,再交给 Tesseract
数据库里执行记录膨胀很快每次跑完保留全量明细写定时任务,清理 30 天前的记录,报告表保留最近 100 条即可

最后一个比较隐蔽的问题:pytest-xdist下每个 worker 单独跑 fixture 的 session 作用域,如果初始化测试数据的 fixture 在多个模块里定义,每个 worker 都执行一次,重复数据会导致断言失败。解决办法是让初始化操作幂等,比如先删除再插入,或者在执行入口处加一个分布式锁。对这套系统,用 SQLite 时把数据库改成 WAL 模式也能减少并发写入时的锁冲突:

import sqlite3 conn = sqlite3.connect("test_data.db") conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL")

WAL 模式在读多写少的场景下能明显降低同一时间多个 worker 操作数据库时的database is locked报错频率;synchronous=NORMAL在可容忍断电丢失最近一次提交的前提下减少同步开销,自动化测试数据丢失一点可以重新生成,性能收益更值得。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询