1. 项目缘起与测试范围拆解
1.1 为什么选一个听书App作为测试实战标的
很多人找测试项目练手,第一反应是电商后台或者管理系统。这类项目确实经典,但问题也很明显:同质化严重,面试官一听就知道是教程里的东西,问不出深度。我选择“不爱听书”这个项目做测试实战,恰恰是因为它的业务形态比普通增删改查复杂——它融合了音频流媒体、用户行为追踪、书架同步、离线缓存这四类典型移动端难题。听书App的核心链路是“搜索→试听→加入书架→断点续传→倍速播放→定时关闭”,每一步都涉及前端、后端、音频引擎、本地数据库的多方交互,非常适合拿来练习接口测试、UI自动化、性能压测。
这个项目源码是基于前后端分离架构搭的,前端是Vue2的移动端页面加Android原生壳,后端是SpringBoot提供RESTful接口,音频文件走对象存储直链或者CDN分发。测试范围覆盖了账号体系、书籍管理、播放控制、社交分享、支付订阅五大模块。你拿到源码之后,不需要从零写业务代码,只需要聚焦在“怎么测”上。说实话,市面上能把测试教程和可运行源码绑在一起的项目不多,大部分只给PPT或者视频,你照着敲完还是不知道真实项目长什么样。这个项目的价值就在于源码可跑、接口可调、数据库可查,你能看到真实的数据流转,而不是对着Mock数据瞎猜。
1.2 技术栈速览与测试切入点
先把这个项目的技术栈理清楚,不然你连日志都看不懂。后端:SpringBoot 2.x + MyBatis-Plus + MySQL 8 + Redis + MinIO(存音频和封面)。前端:Vue 2 + Vant UI + Axios,移动端壳是Android WebView加原生音频组件。接口风格是标准的RESTful JSON,登录鉴权用JWT,Token放在Header的Authorization字段里。测试切入点我建议按这个顺序来:先把接口层跑通,再做UI自动化,最后补性能和安全。为什么这么排?因为UI测试依赖数据准备,数据准备靠接口调用最快;而且接口测试的投入产出比最高,一个脚本能覆盖几十个用例。
提示:拿到源码后先别急着写用例,用Postman把登录、获取书籍列表、加入书架这三个接口手动调通。这一步能帮你确认环境是否正常,省去后面排查环境问题的时间。
1.3 测试环境搭建清单与避坑要点
搭建环境是新手最容易卡住的地方。我把需要的工具列一下,版本尽量对齐,不然会出现莫名其妙的依赖冲突。JDK用1.8或者11,Maven 3.6以上,MySQL 5.7或8.0,Redis 5以上,Node.js 14到16之间(Vue2对高版本Node兼容性一般)。Android端测试需要Appium 1.22左右,配合UiAutomator2驱动。特别注意:MySQL 8的连接URL要加serverTimezone=Asia/Shanghai,否则后端启动会报时区错误;Redis如果设了密码,配置文件里要同步改,不然Token校验会失败。我踩过的坑是MinIO的Endpoint配置——源码里写的是内网地址,你本地跑要改成127.0.0.1:9000,并且提前建好bucket,否则上传封面图会返回403。
# 后端启动前先确认三个服务 mysql -uroot -p -e "show databases;" # 确认不爱听书的库存在 redis-cli ping # 返回PONG curl http://127.0.0.1:9000/minio/health/live # MinIO健康检查环境通了之后,用浏览器访问后端Swagger地址(通常是/doc.html或/swagger-ui.html),能看到接口列表就说明后端OK。前端用npm run serve启动,手机模拟器访问http://你的IP:8080。这里注意:Android模拟器访问本机要用10.0.2.2而不是localhost,这个细节能让你少查半小时资料。
2. 接口测试实战:从手工调通到脚本覆盖
2.1 接口清单梳理与优先级划分
打开Swagger或者抓包工具,先把接口按模块列出来。我一般用Excel或者在线文档建一个接口清单表,字段包括:接口路径、请求方法、入参、出参、依赖关系、优先级。不爱听书这个项目大概有40多个接口,但核心链路只有十几个。优先级怎么定?P0是登录、书籍列表、播放地址获取、书架增删,这些挂了整个App就废了;P1是搜索、评论、分享、订阅状态查询;P2是历史记录、偏好设置、意见反馈。先做P0,保证主流程能跑通,再做P1补充场景。
| 模块 | 核心接口 | 方法 | 鉴权 | 优先级 |
|---|---|---|---|---|
| 用户 | /api/user/login | POST | 否 | P0 |
| 书籍 | /api/book/list | GET | 是 | P0 |
| 播放 | /api/play/url/{bookId} | GET | 是 | P0 |
| 书架 | /api/shelf/add | POST | 是 | P0 |
| 搜索 | /api/search | GET | 是 | P1 |
| 评论 | /api/comment/add | POST | 是 | P1 |
2.2 用Requests+Pytest搭建接口测试骨架
手工调通之后,立刻转成自动化脚本,不然每次回归都要点一遍,时间全浪费在重复劳动上。我用的组合是Python的Requests库加Pytest框架,再加Allure出报告。目录结构这样分:common放配置和日志,api放接口封装,testcases放用例,data放测试数据。先写一个BaseApi类处理Token和公共请求头,后面所有接口继承它。
import requests class BaseApi: def __init__(self): self.base_url = "http://127.0.0.1:8080" self.session = requests.Session() self.token = None def login(self, username, password): url = f"{self.base_url}/api/user/login" payload = {"username": username, "password": password} resp = self.session.post(url, json=payload) self.token = resp.json()["data"]["token"] self.session.headers.update({"Authorization": self.token}) return resp这段代码里有个细节值得说:用requests.Session()而不是每次requests.post(),因为Session会自动保持Cookie,而且连接池复用能提升执行速度。Token拿到后统一塞进headers,后续接口就不用每个都传了。登录密码在源码里通常是MD5加盐存的,测试环境的账号密码可以查数据库的user表,或者直接看源码里的初始化SQL。
2.3 参数化与数据驱动的落地方法
接口用例最怕写死数据。比如“加入书架”这个接口,如果你每次都用bookId=1,跑第二遍就可能因为重复添加而失败。解决办法是数据驱动:把测试数据放在YAML或者CSV里,用Pytest的@pytest.mark.parametrize注入。我一般会准备三组数据:正常值、边界值、异常值。以书架添加为例,正常值是有效bookId,边界值是书架数量达到上限(源码里限制200本),异常值是bookId不存在或者负数。
import pytest import yaml from api.shelf_api import ShelfApi def load_data(): with open("data/shelf_data.yaml") as f: return yaml.safe_load(f) @pytest.mark.parametrize("case", load_data()) def test_add_shelf(case): api = ShelfApi() api.login("tester", "123456") resp = api.add_shelf(case["bookId"]) assert resp.json()["code"] == case["expect_code"]注意:每次执行前要清理测试账号的书架数据,否则用例会相互污染。我习惯在conftest.py里写一个fixture,登录后先调删除接口把书架清空,这个“前置清理”能解决80%的偶发失败。
2.4 接口断言策略与常见陷阱
新手写断言往往只看HTTP状态码200就过了,这是大忌。真实的接口测试要分三层断言:HTTP状态码、业务状态码、数据字段内容。不爱听书的接口返回体是{"code":0,"msg":"success","data":{...}},所以你要断言code等于0,还要断言data里的关键字段符合预期。比如获取播放地址接口,data里要有audioUrl且以http开头,duration要大于0。另一个陷阱是时间戳和随机数——有些接口返回的orderNo或者traceId每次不同,断言时要用正则或者只判断长度。还有分页接口,第一页的数据条数要等于pageSize,最后一页要小于等于pageSize,这个边界很容易漏测。
3. UI自动化测试:移动端页面元素定位与稳定性
3.1 移动端自动化方案选型对比
移动端UI自动化有三条路:Appium、Airtest、UiAutomator原生。Appium跨语言跨平台,社区资料最多,适合这个项目;Airtest基于图像识别,对游戏或者Canvas渲染友好,但维护成本高;UiAutomator只支持Android且要写Java。我选Appium加Python客户端,原因是源码里的Android壳是标准WebView加原生控件,Appium能自由切换Native和WebView上下文,定位方式灵活。版本搭配:Appium Server 1.22.3,appium-python-client 2.x,UiAutomator2驱动。注意Appium 2.x之后驱动要单独安装,命令是appium driver install uiautomator2,这个变化让很多人卡在启动环节。
3.2 元素定位的优先级与实战代码
定位元素的原则是:ID > Accessibility ID > XPath > Class Name。ID最稳,XPath最灵活但最容易碎。不爱听书的播放页有个“播放/暂停”按钮,源码里通常有resource-id,优先用它。如果拿不到ID,用content-desc(Accessibility ID)也比XPath强。实在要用XPath,尽量用相对路径加属性组合,避免绝对路径。下面是一个登录加播放的示例:
from appium.webdriver.common.appiumby import AppiumBy def test_play_audio(driver): driver.find_element(AppiumBy.ID, "com.buaishu:id/et_username").send_keys("tester") driver.find_element(AppiumBy.ID, "com.buaishu:id/et_password").send_keys("123456") driver.find_element(AppiumBy.ID, "com.buaishu:id/btn_login").click() # 等待首页加载 driver.implicitly_wait(10) driver.find_element(AppiumBy.XPATH, "//*[@text='三体']").click() play_btn = driver.find_element(AppiumBy.ID, "com.buaishu:id/btn_play") play_btn.click() assert play_btn.get_attribute("selected") == "true"3.3 等待机制与滑动操作的坑
移动端最大的敌人是网络延迟和动画。你刚点完登录,下一页还没渲染出来,脚本就去找元素,必然报NoSuchElement。解决办法是用显式等待(WebDriverWait)替代强制等待(sleep)。比如等待“我的书架”标题出现,最多等15秒,每0.5秒查一次。另外,列表滑动不要用坐标硬滑,用driver.swipe()在不同分辨率手机上会失效。推荐用UiAutomator的scroll方法或者Appium的touch_action配合元素定位。还有一个坑:WebView里的元素需要先driver.switch_to.context("WEBVIEW_com.buaishu"),操作完再切回NATIVE_APP,忘了切回来后面全部报错。
实操心得:调试脚本时打开Appium Inspector,它能实时查看页面元素树。但是注意,Inspector会和你的脚本抢驱动端口,调试完要关掉Inspector再跑脚本,否则报“session is already running”。
3.4 测试用例分层与数据清理
UI用例不要写成一条超长流程,要分层。我分成三类:冒烟用例(登录→搜索→播放→退出)、功能用例(每个页面独立验证)、异常用例(断网、空数据、权限拒绝)。每类用例执行前用@BeforeMethod重置App状态,用driver.reset()或者adb shell pm clear清数据。数据清理同样重要,UI操作产生的书架数据、历史记录要删掉,不然下次跑用例首页全是脏数据。我的做法是UI用例跑完后,调一次接口把账号数据重置,这比在UI上点删除快得多,也稳定得多。
4. 性能压测与专项测试的取舍
4.1 听书App的性能指标该盯哪几个
性能测试不是所有项目都要做,但听书App有两个指标绕不开:音频首播加载时间和并发播放稳定性。首播加载时间指从点击播放到声音出来的间隔,超过3秒用户就会烦躁,超过5秒直接卸载。并发播放稳定性指100人同时请求音频流时,服务端带宽和CDN回源是否扛得住。用JMeter做压测,线程组设100,Ramp-up设10秒,循环5次,监控TPS和响应时间。源码里音频走MinIO直链,压测时你会发现瓶颈往往在磁盘IO或者带宽,而不是CPU。如果响应时间随并发数线性上升,说明带宽打满了;如果突然大量报错,检查MinIO的连接数配置。
4.2 弱网模拟与断点续传验证
移动端专项测试里,弱网是必测项。用Charles或者Fiddler设置限速,模拟2G/3G/4G和丢包场景。重点验证断点续传:播放到30秒时断网,恢复后能不能从30秒继续,而不是从头开始。源码里断点续传依赖本地数据库记录播放位置,测试时要检查SharedPreferences或者SQLite里的position字段有没有更新。我遇到过恢复网络后位置没同步的Bug,原因是后台线程被杀死,位置没写进去。这种问题只能靠专项测试发现,功能测试覆盖不到。
4.3 兼容性测试的轻量方案
兼容性测试不等于买一堆手机。预算有限的话,用Android模拟器覆盖主流分辨率(720P、1080P、2K)和Android版本(7.0、10、13),再用云测平台补几台真机。重点看三个地方:播放页布局是否错位、音频焦点切换是否正常、后台播放是否被杀死。音频焦点问题是听书App特有的——你正听着书,来了一个电话,挂断后能不能自动恢复播放。这个在源码里通常用AudioManager处理,测试时要专门设计用例覆盖。
5. 实战答疑与避坑速查
5.1 常见问题排查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 后端启动报时区错误 | MySQL URL缺serverTimezone | 加serverTimezone=Asia/Shanghai |
| 登录返回401 | Token未带或过期 | 检查Header的Authorization字段 |
| 播放无声音 | MinIO音频地址403 | 检查bucket权限和Endpoint配置 |
| Appium找不到元素 | 未切换WebView上下文 | switch_to.context切换 |
| 压测TPS上不去 | 带宽或磁盘IO瓶颈 | 看服务器监控,升级带宽或换SSD |
| 书架数据重复 | 用例未清理前置数据 | conftest里加清理fixture |
5.2 面试中怎么讲这个项目
这个项目拿去找工作,面试官一定会问:“你测了什么?发现了什么Bug?”别只说“我写了多少用例”,要说业务价值。比如:“我发现播放页在弱网下断点续传失败,定位到是本地位置写入被后台线程竞争锁阻塞,修复后用户流失率预估降低X%。”这种回答有场景、有定位过程、有影响分析,比背测试理论强十倍。另外准备好两个数据:接口自动化覆盖率(比如核心接口80%)、UI自动化节省的回归时间(比如从2小时缩到20分钟)。数字最直观。
5.3 源码二次开发的测试扩展
拿到源码别只跑一遍就完事,试着改点东西再测。比如把书架上限从200改成500,看前端有没有同步改校验;把音频码率调高,看弱网下卡顿是否加剧。这种“破坏性测试”能锻炼你的测试思维。还有一个方向是接入CI/CD:用Jenkins拉代码、跑接口脚本、出Allure报告、发邮件通知。整套流程跑通之后,你对“测试左移”和“持续集成”的理解会完全不一样,这才是项目实战的真正价值。
最后分享一个我踩过的坑:跑UI自动化时,模拟器突然卡死,脚本报“socket hang up”。后来发现是模拟器内存分配太小,改成4G内存加2核CPU后稳如老狗。所以环境配置别抠门,该给的资源要给够,否则调脚本的时间比写脚本还长。