简介
本文件面向 DouPHP 框架的服务层,提供一套可落地的测试策略与最佳实践,覆盖单元测试、集成测试与端到端测试。内容涵盖:
- 测试环境搭建(数据库、缓存、外部服务模拟)
- Mock 对象与替身的使用(ORM 门面、第三方服务、文件系统)
- 数据库测试(事务回滚、种子数据、隔离策略)
- 外部服务模拟(支付、短信、云存储等)
- 高质量用例编写规范(命名、断言、边界条件、并发与幂等)
- 覆盖率目标、持续集成配置与自动化流程
- 调试技巧与常见问题定位
项目结构
DouPHP 采用“三端入口 + 共享核心 + 模块化”的架构。服务层主要位于 core/service 以及各端(admin/front/api)的 service 目录下,通过 BaseService 统一抽象,并通过 ORM 静态门面访问数据。
graph TB
A["前端/后台/API 控制器"] --> B["业务服务层<br/>core/service/*"]
B --> C["子服务/查询器/守卫<br/>按主题域拆分"]
C --> D["ORM 静态门面<br/>core/facade/DB.php"]
D --> E["数据库"]
B --> F["外部服务<br/>支付/短信/云存储"]
F --> G["第三方系统"]
图表来源
- core/service/BaseService.php:21-43
- core/facade/DB.php
章节来源
- README.md:15-26
- core/service/BaseService.php:21-43
核心组件
- 服务基类 BaseService:定义服务层的约定与依赖解析方式,强调不直接读取请求上下文,所有输入由 shell 层显式传入;写操作通过 ORM 静态门面完成。
- 用户服务 UserService:薄编排门面,组合资料读模型、扩展身份、联系人、推广关系等子能力。
- 订单服务 OrderService:薄编排门面,组合购物车、状态机、库存守卫、定时任务、结算选项等子能力。
这些服务体现了“薄门面 + 细粒度子服务”的设计,便于单测聚焦具体行为,也利于集成测试验证跨子服务的协作。
章节来源
- core/service/BaseService.php:21-43
- core/service/user/UserService.php:23-38
- core/service/order/OrderService.php:23-36
架构总览
下图展示了典型的服务调用链:控制器将请求参数转换为领域数据后调用服务,服务通过 ORM 门面读写数据库,必要时调用外部服务(如支付、短信)。
sequenceDiagram
participant Ctrl as "控制器"
participant Svc as "业务服务"
participant DB as "ORM 门面(DB)"
participant Ext as "外部服务(支付/短信/云)"
Ctrl->>Svc : 调用领域方法(入参校验后的数据)
Svc->>DB : 查询/写入(静态门面)
DB-->>Svc : 结果集/影响行数
alt 需要外部交互
Svc->>Ext : 调用第三方接口
Ext-->>Svc : 响应/回调
end
Svc-->>Ctrl : 返回领域结果或错误码
图表来源
- core/service/BaseService.php:21-43
- core/facade/DB.php
详细组件分析
用户服务 UserService 测试要点
- 职责边界:UserService 作为门面,应优先对“组合逻辑”进行断言,例如根据 features 开关决定是否加载 VIP/工作端/分销信息。
- 子服务解耦:UserProfileQuery、UserMembershipQuery、UserContactQuery、UserPromotionService 可单独单测;UserService 单测关注其转发与聚合是否正确。
- 外部依赖:若子服务依赖 ORM 或外部模块,使用替身/桩替换,确保只验证当前门面的编排逻辑。
classDiagram
class UserService {
+buildUserProfile(row, field) array
+levelName(level_id) string
+findUserId(keyword) mixed
+currentLevel(userId) array|null
+format(userOrId) array|false
+work(user_id) array|null
+vip(user_id) array|bool
+distribution(user_id) array|bool
+isVip(userId) bool
+isWork(userId) bool
+isDistribution(userId) bool
+contactList(user_id, current_contact_id) array
+addressFull(contact) string
+resolvePromotionLineage(userSn, selfUserId) array
+recordPromotionRelation(newUserId, directUserId) void
+randUserSn() string
+createAvatarFilename() string
}
图表来源
- core/service/user/UserService.php:71-289
章节来源
- core/service/user/UserService.php:23-38
- core/service/user/UserService.php:71-289
订单服务 OrderService 测试要点
- 状态机与副作用:changeStatus、retryPaidEffects 等方法涉及状态迁移与积分/分销联动,需重点覆盖正常路径与异常路径(幂等性、重复触发保护)。
- 库存守卫:checkStock、realTimeStock 需结合并发场景设计用例,验证锁与一致性。
- 结算选项:getPaymentList/getShippingList 应验证插件启用过滤与排序。
flowchart TD
Start(["进入 changeStatus"]) --> Validate["校验 order_sn 与新状态"]
Validate --> Valid{"有效?"}
Valid -- 否 --> Err["抛出参数异常/返回失败"]
Valid -- 是 --> Transition["执行状态迁移"]
Transition --> Effects{"是否触发付款联动?"}
Effects -- 是 --> SideEffects["派发积分/分销/会员升级等副作用"]
Effects -- 否 --> Commit["提交事务/持久化"]
SideEffects --> Commit
Commit --> End(["返回成功"])
Err --> End
图表来源
- core/service/order/OrderService.php:118-177
章节来源
- core/service/order/OrderService.php:118-177
服务基类 BaseService 的测试启示
- 依赖注入风格:复杂查询/计算下沉到独立服务,便于单测;BaseService 鼓励以 DI 组装,避免在构造中硬编码全局。
- ORM 访问约定:写操作统一 create/update/save,读操作统一 with/find/get/paginate,单测可通过拦截 ORM 门面或替换底层实现来验证行为。
- 安全与校验:CSRF/XSS/Check 等工具在 shell 层处理,服务层专注业务规则,单测无需模拟 HTTP 上下文。
章节来源
- core/service/BaseService.php:21-43
依赖分析
- 服务层依赖 ORM 静态门面进行数据访问,因此单测时可通过替换/拦截 DB 门面或底层 Query Builder 来避免真实数据库访问。
- 外部服务(支付、短信、云存储)通过插件或服务类接入,建议为每个外部依赖定义接口或可替换的适配器,以便在单测中使用替身。
- 配置项(如 features.*、模块开关)会影响服务分支,单测应覆盖不同配置组合。
graph LR
Svc["业务服务"] --> ORM["ORM 门面"]
Svc --> Ext["外部服务适配器"]
Svc --> Conf["配置中心"]
ORM --> DB["数据库"]
Ext --> API["第三方API"]
图表来源
- core/facade/DB.php
- config/config.php
章节来源
- core/facade/DB.php
- config/config.php
性能考虑
- 单测优先:尽量使用内存替身与轻量数据库(SQLite),减少 I/O 开销。
- 批量与分页:对包含大量数据的查询,单测仅验证关键分支与边界值,避免全量数据扫描。
- 事务与锁:并发相关用例应在隔离环境中运行,避免相互干扰。
- 缓存策略:对读多写少的数据,可在集成测试中验证缓存命中与失效逻辑。
故障排查指南
- 数据库连接失败:检查测试环境的数据库配置与权限,确认端口、用户名、密码正确。
- ORM 门面未生效:确认测试引导已加载自动加载与门面注册,必要时在测试套件初始化中引入核心引导。
- 外部服务超时:为第三方接口设置短超时与重试上限,并在失败时快速报错以便定位。
- 事务未回滚:确保测试用例在结束后清理数据,或使用事务包裹整个用例。
- 配置不一致:核对测试环境与生产环境的 features.*、模块开关差异,避免因配置导致分支未覆盖。
结论
基于 BaseService 的约定与“薄门面 + 细粒度子服务”的组织方式,DouPHP 服务层具备良好的可测试性。通过分层测试策略(单元/集成/E2E)、严格的 Mock 与替身管理、合理的数据库与外部服务模拟,以及完善的 CI 流水线,可以持续提升服务层的正确性与稳定性。
附录
测试金字塔与范围
- 单元测试:针对单个服务方法或子服务,速度快、隔离性强,覆盖大部分业务分支。
- 集成测试:验证服务与 ORM、缓存、消息队列、外部服务的协作,使用测试数据库与沙箱接口。
- 端到端测试:覆盖关键用户旅程(如下单、支付回调、售后),用于回归与冒烟。
测试环境搭建
- 数据库:准备独立的测试库,支持事务回滚;可使用 SQLite 加速单测。
- 缓存与队列:使用内存驱动或本地替代,避免污染生产资源。
- 外部服务:使用沙箱账号与 Mock 服务器(如 WireMock、LocalStack)模拟响应。
- 环境变量:通过 .env.test 或配置文件区分测试与生产配置。
Mock 对象与替身
- ORM 门面:拦截 DB::query/Model::where 等调用,返回预设数据集或断言 SQL。
- 外部服务:为支付、短信、云存储等定义接口,测试时使用替身返回固定响应。
- 文件系统:使用内存磁盘或临时目录,避免真实文件 IO。
数据库测试
- 事务回滚:每个用例前后开启/关闭事务,保证数据隔离。
- 种子数据:使用工厂或 Fixture 生成最小必要数据。
- 断言:不仅断言返回值,还要断言数据库状态变化(新增/更新/删除)。
外部服务模拟
- 支付回调:模拟异步通知与签名校验,覆盖成功、失败、重复回调。
- 限流与重试:验证重试策略与退避算法,防止雪崩。
- 幂等性:对重复请求进行幂等处理验证。
高质量用例编写
- 命名:用例名清晰表达“当…时,期望…”。
- 断言:明确输入、输出、副作用与异常。
- 边界条件:空值、极值、非法输入、并发冲突。
- 可读性:Arrange-Act-Assert 三段式组织。
覆盖率要求与报告
- 行覆盖率:≥80%(核心业务 ≥90%)
- 分支覆盖率:≥70%(关键路径 ≥85%)
- 报告:生成 HTML 报告并归档,PR 中展示增量覆盖率。
持续集成与自动化
- 触发:Push/Pull Request 触发构建与测试。
- 矩阵:多 PHP 版本、多数据库版本并行执行。
- 缓存:缓存依赖包与构建产物,缩短构建时间。
- 质量门禁:覆盖率、静态分析、代码风格检查通过后合并。
调试技巧
- 日志:在关键分支记录结构化日志,便于定位问题。
- 断点:本地使用 IDE 断点调试,CI 中输出详细堆栈。
- 回放:保存失败用例的请求/响应快照,便于复现。
- 慢查询:记录耗时超过阈值的查询,优化索引与 SQL。