实战解析:黑马头条自动化测试框架设计与实现
1. 从零开始:为什么黑马头条需要一个自动化测试框架?
大家好,我是老张,在测试这行摸爬滚打十多年了,带过不少团队,也亲手搭建过好几个自动化测试框架。今天想和大家聊聊一个非常接地气的实战项目——黑马头条的自动化测试框架设计与实现。你可能要问,市面上现成的框架那么多,为啥还要自己折腾?这事儿我深有体会。
黑马头条这个项目,它不是一个简单的单体应用,而是一个包含了用户端APP、自媒体运营平台(PC网站)、后台管理系统(PC网站) 的综合性产品。想象一下,每次版本迭代,测试同学要在这三个端之间反复横跳,验证核心功能,比如用户能不能看文章、自媒体作者能不能发文章、后台管理员能不能审核文章。如果全靠手工点,一次回归测试就得花上好几天,不仅人累得够呛,还容易漏测,上线后半夜被报警电话叫醒的滋味可不好受。
所以,我们的目标很明确:把那些重复、稳定、核心的业务流程交给机器去执行,把人解放出来去做更有价值的探索性测试和复杂场景设计。这不仅仅是“偷懒”,更是提升测试效率、保证产品质量稳定性的关键一步。我见过太多团队,一开始兴致勃勃地搞自动化,结果代码写得乱七八糟,维护成本比手工测试还高,最后不了了之。因此,一个设计良好、易于维护的框架是成功的前提。
接下来,我就带你一步步拆解,我们是如何为黑马头条这个多端产品,量身打造一个既稳健又灵活的自动化测试框架的。我会分享我们当时的设计思路、踩过的坑,以及最终沉淀下来的可复用的代码结构。无论你是刚入门自动化测试的新手,还是想优化现有框架的老兵,相信都能从中获得一些启发。
2. 谋定而后动:自动化测试框架的顶层设计与核心思想
在动手写第一行代码之前,我们花了相当长的时间来讨论和设计框架的整体架构。这一步至关重要,它决定了后续开发是顺风顺水还是举步维艰。我们的核心设计思想可以概括为:“多端隔离、页面对象模式、分层封装、配置驱动”。
首先,多端隔离是基础。 黑马头条的三个前端产品(APP、自媒体平台、后台管理)技术栈和交互方式不同。自媒体和后台是Web,用Selenium;APP是移动端,用Appium。如果混在一起管理驱动和页面,代码会很快变得混乱不堪。我们的做法是为每个端建立独立的“驱动管理中心”和“基础操作层”,从根源上隔离。这样,为自媒体平台写的测试脚本,完全不会影响到APP端的执行环境。
其次,我们坚定地采用了经典的“页面对象模式(Page Object Model, POM)”。 这是减少代码冗余、提升可维护性的不二法门。简单说,就是把每个页面(如登录页、首页、发布文章页)当成一个对象,页面上的元素定位就是它的属性,页面上的操作(输入、点击)就是它的方法。当页面UI发生变化时,比如一个按钮的ID改了,我们只需要去修改对应的页面对象类,而所有用到这个按钮的测试脚本完全不用动。这在实际项目中救了我们无数次。
再者,分层封装让结构更清晰。 我们把代码分为几个层次:
- 基础工具层(Utils):管理浏览器/APP驱动,封装一些通用方法(如等待元素、选择下拉框)。
- 页面对象层(Page):对应POM模式,这里又细分为“对象库”(只负责定位元素)和“操作层”(对元素进行点击、输入等操作)。
- 业务层(Proxy):这是我们的一个特色封装。它把操作层的多个步骤组合成一个完整的业务操作。比如“登录”这个业务,就包含了“输入用户名”、“输入密码”、“点击登录按钮”三个操作。测试脚本直接调用业务层的方法,读起来就像在看自然语言,非常直观。
- 测试脚本层(Scripts):这里就是真正的测试用例,使用pytest等测试框架来组织,只关心测试数据、业务调用和断言。
最后,配置驱动提升灵活性。 我们把浏览器类型、APP的Desired Capabilities、测试环境的URL地址、等待超时时间等都放到配置文件里。这样,同一套脚本,通过修改配置就能轻松地在测试环境、预发布环境甚至不同的浏览器上运行,避免了在代码里写死这些参数。
有了这些顶层设计,我们就好比有了施工图纸,接下来就是准备“建筑材料”和“施工环境”了。
3. 工欲善其事:一站式环境搭建与项目初始化
环境搭建是新手最容易卡住的地方,我们力求把这个过程做到最简。我们的框架主要支持Web和APP自动化,所以环境也围绕这两块来准备。
对于Web自动化(自媒体平台和后台管理):
- Python环境:这是我们的脚本语言,选择3.7以上版本。我推荐直接用PyCharm作为IDE,它对项目管理、虚拟环境支持都很好。
- 浏览器与驱动:我们主要用Chrome。关键一步是下载和你Chrome浏览器版本匹配的
chromedriver,并把它放在系统PATH路径下,或者直接在代码里指定路径。这一步不匹配,后面会报各种找不到驱动的错误,我踩过这个坑。 - 核心Python包:通过
pip安装几个必备的库。pip install selenium # Web自动化核心 pip install pytest # 测试框架,用来组织和运行用例 pip install pytest-html # 生成HTML测试报告 pip install pytest-rerunfailures # 失败重试,应对网络不稳定等情况
对于APP自动化(用户端APP):
- 基础环境:需要安装Java JDK和Android SDK,用于提供Appium所需的运行环境。
- Appium Server:可以桌面版,也可以用命令行安装。它是一个Node.js应用,负责接收我们的测试脚本指令,转发给手机或模拟器。
npm install -g appium - 模拟器或真机:开发测试阶段,用Android模拟器(如Android Studio自带的)非常方便。记得开启开发者选项和USB调试模式。
- Python客户端库:
pip install Appium-Python-Client
环境准备好后,我们开始初始化项目。我强烈建议从一开始就规划好清晰的目录结构,这对团队协作和后期维护有巨大好处。这是我们当时创建的项目骨架:
uiAutoTestHmtt/ # 项目根目录
├── base/ # 基础封装层,存放各端的基类
│ ├── mp/ # 自媒体平台基类
│ ├── mis/ # 后台管理系统基类
│ └── app/ # APP基类
├── page/ # 页面对象层,按端再分子目录
│ ├── mp/ # 自媒体平台页面对象
│ ├── mis/ # 后台管理系统页面对象
│ └── app/ # APP页面对象
├── scripts/ # 测试脚本层
│ ├── mp/ # 自媒体平台测试用例
│ ├── mis/ # 后台管理系统测试用例
│ └── app/ # APP测试用例
├── data/ # 存放测试数据文件,如JSON、Excel
├── report/ # 存放自动生成的测试报告
├── log/ # 存放运行日志
├── screenshot/ # 存放失败用例的截图
├── utils.py # 工具函数,如驱动管理、通用方法
├── config.py # 配置文件,放URL、账号等
├── pytest.ini # pytest配置文件
└── conftest.py # pytest的共享fixture配置
这个结构一目了然,新人加入项目,很快就能知道该去哪里找对应的代码。接下来,我们就要往这个骨架里填充最有灵魂的部分——驱动管理和基础封装。
4. 框架基石:驱动管理与基础类的精妙封装
这是整个框架最核心、最体现设计功力的部分。我们封装的目标是:让测试脚本编写者完全不用操心驱动从哪里来、怎么关、元素怎么找、操作怎么做这些底层细节。
首先,我们实现了一个全局的驱动管理工具类(UtilsDriver)。 它的核心思想是单例模式,确保在整个测试运行过程中,同一个端的驱动只有一个实例,避免重复创建和端口冲突。同时,它提供了优雅的退出机制。
# utils.py 中的关键部分
from selenium import webdriver
from appium import webdriver as appdriver
class UtilsDriver:
# 使用类变量来保存驱动实例,实现单例
_mp_driver = None # 自媒体平台驱动
_mis_driver = None # 后台管理系统驱动
_app_driver = None # APP驱动
@classmethod
def get_mp_driver(cls):
"""获取自媒体平台Web驱动"""
if cls._mp_driver is None: # 如果驱动不存在,才创建
cls._mp_driver = webdriver.Chrome()
cls._mp_driver.maximize_window()
cls._mp_driver.get("http://ttmp.research.itcast.cn/") # 配置化URL更好
return cls._mp_driver
@classmethod
def quit_mp_driver(cls):
"""退出自媒体平台驱动"""
if cls._mp_driver is not None:
cls._mp_driver.quit()
cls._mp_driver = None # 置空,防止下次使用旧引用
# ... 类似的 get_mis_driver, get_app_driver 方法
对于APP驱动,配置稍微复杂点,但原理一样:
@classmethod
def get_app_driver(cls):
if cls._app_driver is None:
des_cap = {
"platformName": "Android",
"platformVersion": "10", # 根据你的模拟器调整
"deviceName": "emulator-5554",
"appPackage": "com.itcast.toutiaoApp",
"appActivity": ".MainActivity",
"noReset": True, # 非常重要!避免每次重启APP清空数据
"unicodeKeyboard": True # 支持中文输入
}
cls._app_driver = appdriver.Remote("http://127.0.0.1:4723/wd/hub", des_cap)
return cls._app_driver
然后,我们为每个端封装了它们的基础类(BasePage和BaseHandle)。 这是页面对象模式的起点。BasePage负责元素的查找,并内置了显式等待,让元素定位更稳定。BaseHandle提供最基础的元素操作,如输入文本。
# base/mp/base.py 示例
from selenium.webdriver.support.wait import WebDriverWait
from utils import UtilsDriver
class BasePage:
"""自媒体平台-页面对象层基类"""
def __init__(self):
# 直接获取全局唯一的驱动实例
self.driver = UtilsDriver.get_mp_driver()
def get_element(self, location):
"""封装查找元素,加入显式等待"""
wait = WebDriverWait(self.driver, timeout=10, poll_frequency=0.5)
element = wait.until(lambda x: x.find_element(*location))
return element
class BaseHandle:
"""自媒体平台-操作层基类"""
@staticmethod
def input_text(element, text):
"""封装输入操作,先清空再输入"""
element.clear()
element.send_keys(text)
# 还可以封装 click, get_text 等方法
为mis(后台管理)和app端封装类似的基类,只是__init__中获取驱动的方法分别改为UtilsDriver.get_mis_driver()和UtilsDriver.get_app_driver()。这样,各端的页面对象类继承自各自的基类,天然就获得了正确的驱动和基础方法。基础打牢了,后面建造“页面”这座大楼就快多了。
5. 实战演练:以自媒体平台为例拆解页面对象封装
理论说再多,不如看一个完整的例子。我们就拿自媒体平台最核心的“登录->进入首页->发布文章”这个流程来拆解,看看如何将POM模式和我们的分层思想落地。
第一步:封装登录页面(login_page.py)
我们把这个页面放到page/mp/目录下。严格按照对象库层、操作层、业务层来写。
# page/mp/login_page.py
from base.mp.base import BasePage, BaseHandle
from selenium.webdriver.common.by import By
# 1. 对象库层:只关心这个页面上有哪些元素,以及如何找到它们
class LoginPage(BasePage):
def __init__(self):
super().__init__() # 初始化父类,获得self.driver
# 使用By类来定义元素定位器,这是最佳实践
self.mobile_loc = (By.XPATH, "//input[@placeholder='请输入手机号']")
self.code_loc = (By.XPATH, "//input[@placeholder='验证码']")
self.login_btn_loc = (By.CSS_SELECTOR, ".el-button--primary")
def find_mobile(self):
"""定位手机号输入框"""
return self.get_element(self.mobile_loc)
def find_code(self):
"""定位验证码输入框"""
return self.get_element(self.code_loc)
def find_login_btn(self):
"""定位登录按钮"""
return self.get_element(self.login_btn_loc)
# 2. 操作层:对找到的元素执行具体操作
class LoginHandle(BaseHandle):
def __init__(self):
self.login_page = LoginPage() # 组合一个页面对象
def input_mobile(self, mobile):
self.input_text(self.login_page.find_mobile(), mobile)
def input_code(self, code):
self.input_text(self.login_page.find_code(), code)
def click_login_btn(self):
self.login_page.find_login_btn().click()
# 3. 业务层:将操作组合成有业务意义的流程
class LoginProxy:
def __init__(self):
self.login_handle = LoginHandle()
def login(self, mobile, code):
"""登录业务:输入手机号、验证码,点击登录"""
self.login_handle.input_mobile(mobile)
self.login_handle.input_code(code)
self.login_handle.click_login_btn()
# 这里可以加入等待登录成功的逻辑
第二步:封装首页和发布文章页 模式是一样的。首页(home_page.py)需要定位用户信息(用于断言登录成功)、内容管理菜单和发布文章按钮。发布文章页(publish_page.py)则复杂一些,因为它涉及iframe切换(富文本编辑器)、下拉框选择等。
在封装发布文章页的下拉框选择时,我们把这个通用操作抽到了utils.py里,因为三个端都可能用到。
# utils.py 补充
import time
from selenium.webdriver.common.by import By
def select_by_text(driver, trigger_element, text):
"""
通用下拉框选择方法
:param driver: 浏览器驱动
:param trigger_element: 触发下拉框显示的元素(比如那个输入框)
:param text: 要选择的选项文本
"""
trigger_element.click()
time.sleep(0.5) # 等待下拉动画,可以用显式等待优化
# 构造XPath定位到下拉列表中文本匹配的选项
option_xpath = f"//div[contains(@class, 'el-select-dropdown')]//span[text()='{text}']"
driver.find_element(By.XPATH, option_xpath).click()
第三步:编写测试用例脚本 页面对象都封装好后,写测试用例就变成了一件非常愉快的事情,读起来就像写自然语言一样。
# scripts/mp/test_publish_article.py
import pytest
from page.mp.login_page import LoginProxy
from page.mp.home_page import HomeProxy
from page.mp.publish_page import PublishProxy
from utils import UtilsDriver
class TestPublishArticle:
def setup_class(self):
"""测试类开始前执行:初始化所有业务对象"""
self.login_proxy = LoginProxy()
self.home_proxy = HomeProxy()
self.publish_proxy = PublishProxy()
def teardown_class(self):
"""测试类结束后执行:关闭驱动"""
UtilsDriver.quit_mp_driver()
def test_login(self):
"""测试登录功能"""
self.login_proxy.login("13800138000", "123456")
# 断言:首页是否显示了正确的用户名
username = self.home_proxy.get_username_msg()
assert username == "测试账号"
def test_publish_article(self):
"""测试发布文章功能"""
# 1. 从首页跳转到发布页
self.home_proxy.go_to_publish_page()
# 2. 执行发布文章业务
self.publish_proxy.publish_article(
title="自动化测试发布的文章",
content="这里是文章内容,由自动化测试脚本自动填充。",
channel="前端开发" # 选择频道
)
# 3. 断言:页面是否出现“发布成功”的提示
# 这里可以调用一个通用的文本判断工具
assert self.utils.is_text_present("发布成功")
你看,在测试用例里,我们完全看不到任何find_element、send_keys、click这样的底层API,也看不到复杂的元素定位表达式。我们只关心业务:登录、跳转、发布、断言。这样的代码,可读性极高,维护成本极低。当页面UI改动时,我们只需要去修改对应的page下的页面对象类,而这些测试脚本几乎不用动。这就是分层封装和POM模式带来的最大好处。
6. 高效执行与报告:用pytest组织测试并生成漂亮报告
框架和用例写好了,怎么方便地运行它们并直观地看到结果呢?我们选择了pytest作为测试执行框架,它比Python自带的unittest更强大、更灵活。
首先,配置pytest(pytest.ini)。 这个配置文件可以让我们自定义pytest的默认行为。
# pytest.ini
[pytest]
# 默认命令行参数
addopts = -v -s --html=report/report.html --reruns 2 --reruns-delay 1
# -v: 详细输出
# -s: 打印print信息
# --html: 生成HTML报告到指定路径
# --reruns: 失败用例重试2次,这对付前端偶尔的网络抖动或加载慢特别有用
# --reruns-delay: 重试间隔1秒
# 指定测试用例的搜索路径
testpaths = ./scripts
# 指定测试文件名的模式
python_files = test_*.py
# 指定测试类的模式
python_classes = Test*
# 指定测试函数/方法的模式
python_functions = test_*
然后,使用conftest.py来管理共享的fixture。 Fixture是pytest的精华,用于测试前置和后置工作。比如,我们可以把每个用例都需要“登录”这个步骤写成一个fixture。
# conftest.py
import pytest
from page.mp.login_page import LoginProxy
from page.mp.home_page import HomeProxy
@pytest.fixture(scope='class')
def login_and_to_home():
"""一个类级别的fixture:完成登录并返回首页业务对象"""
login_proxy = LoginProxy()
home_proxy = HomeProxy()
login_proxy.login("13800138000", "123456")
yield home_proxy # yield之前是前置,之后是后置
# 这里可以做一些清理工作,比如退出登录(如果提供的话)
这样,在测试类中就可以直接使用这个fixture了:
class TestArticleManagement:
def test_something(self, login_and_to_home):
home_proxy = login_and_to_home
# 现在home_proxy已经是登录后的状态了,可以直接使用
最后,运行测试并查看报告。 在项目根目录下,只需要执行一条命令:
pytest
pytest会根据pytest.ini的配置,自动发现scripts目录下所有以test_开头的文件,并运行其中以test_开头的函数或方法。运行结束后,会在report目录下生成一个report.html文件。用浏览器打开这个文件,你能看到一个非常清晰的测试结果总览,包括通过数、失败数、跳过数,以及每个失败用例的详细错误堆栈。这对于团队分享测试结果、排查问题非常有帮助。
我们还可以集成更强大的报告工具,比如Allure,它能生成更加美观、交互性更强的报告,并且支持展示截图、附件等。不过对于项目初期,pytest-html生成的报告已经足够清晰实用了。关键在于,整个执行和报告生成的过程是完全自动化的,这才是我们追求的效率。
7. 避坑指南与最佳实践:来自实战的经验之谈
框架搭起来了,用例也能跑了,但要想让它长期稳定地为项目服务,还需要注意很多细节。这里分享几个我们踩过坑后总结出来的最佳实践。
1. 元素定位策略要稳健且可维护。
- 优先级:ID > Name > CSS Selector > XPath。ID和Name是首选,但现代前端框架生成的ID可能很长且动态变化,这时CSS Selector通常比复杂的XPath更高效、更易读。
- 慎用绝对XPath:绝对路径(以
/开头)极度脆弱,页面结构稍有变动就会失效。尽量使用相对XPath(如//button[contains(@class, 'submit')])或结合其他属性。 - 处理动态元素:对于加载慢或动态渲染的元素,一定要用
WebDriverWait进行显式等待,而不是用time.sleep硬等待。显式等待更智能,元素一出现就继续,不会浪费多余时间。
2. 测试数据要与代码分离。
不要把测试账号、密码、文章标题等数据硬编码在测试脚本里。应该放在data目录下的JSON、YAML或Excel文件中,或者通过config.py配置文件管理。这样,切换测试环境时,改一下配置文件即可。
3. 失败截图和日志是救命稻草。
测试在CI/CD流水线中运行时,失败了你怎么知道当时页面是什么样子?我们要求在conftest.py中写一个自动捕获失败截图的fixture,并保存到screenshot目录,以用例名和时间戳命名。同时,使用Python的logging模块记录关键操作步骤到log文件。这样,排查问题就有据可依了。
4. 保持用例的独立性。
每个测试用例都应该可以独立运行,不依赖于其他用例的执行状态。这意味着你需要在setup/teardown或者fixture中处理好测试环境,比如每个用例都使用独立的测试账号,或者用例执行后清理掉自己产生的数据(如删除测试文章)。依赖性强是自动化测试套件后期难以维护的主要原因之一。
5. 平衡自动化的范围。 不是所有功能都适合自动化。遵循“二八原则”:用20%的精力覆盖80%最核心、最稳定的业务流程。像发布文章、登录、审核这类高频核心功能,是自动化的首选。而一些UI频繁变动、逻辑极其复杂、或者一次性测试的功能,则更适合手工测试。盲目追求覆盖率只会让维护成本爆炸。
6. 代码也需要Review和重构。 测试代码也是代码,它应该具备生产代码一样的质量要求。定期进行代码审查,看看是否有定位器可以优化、是否有重复代码可以抽取、是否有更清晰的写法。随着项目迭代,页面对象和业务流也会变化,测试框架也需要适时重构,保持其活力。
搭建自动化测试框架就像养一个孩子,不是一蹴而就的,需要持续的投入和精心的维护。但当你看到每次代码提交后,自动化测试套件在后台静静运行,快速反馈核心功能是否正常,那种安心感和解放出来的生产力,会让你觉得所有的付出都是值得的。黑马头条的这个框架,至今还在稳定运行并不断演进,支撑着产品的快速迭代。希望我的这些实战解析,能帮你少走一些弯路。
更多推荐


所有评论(0)