全栈编排与全局启动
YouTeacher 不是一个「启动了就能对着它跑测试」的单一服务。它是一支舰队——一个 Next.js web 前端,前面挡着彼此独立的 auth、job、profile、talent、content、admin、discord 和 games-BFF 各服务,其中几个还各自配了后台 worker,而这一切都坐在一个 nginx 网关之后。集成测试的这套 harness 存在的意义,就是把整支舰队在一台机器上立起来、逐一证明每一块都活着、把上一轮遗留的残渣清干净,然后才让单一的 Playwright 进程驱动浏览器走完全程。它的职责是让「这个平台」表现得像一个地址:要么端到端能用,要么在测试出错之前先大声地失败。
两份 compose 文件,层叠而成
整个栈由两份彼此层叠的 Docker Compose 文件来描述。基础文件负责把应用服务和网关拉起来。第二份「本地基础设施」文件覆盖其上:它补上有状态的依赖——PostgreSQL(一个实例托管多个服务的数据库)、Redis、Meilisearch,以及用作对象存储的 MinIO——并且改写每个服务的启动命令,让它们在启动前先跑一次 Prisma schema push。一个 --remoteDB 开关会去掉第二份文件,于是同一批应用服务转而指向远端基础设施。层叠的好处,是把「这些服务是什么」和「它们的数据放在哪里」两件事的定义相互分开。
宿主网络与网关
每个容器都跑在宿主网络(host network)上,而不是私有桥接网络。这意味着每个服务直接绑定一个固定的 localhost 端口——auth、job、profile、talent、content、admin、discord 以及 games BFF 各自占据 3000 段里的一个端口——测试运行方通过 localhost 直接够到它们,中间没有 Docker DNS。它们前面坐着一个单一的 nginx 反向代理,即网关,它纯粹按 URL 路径前缀路由:每个服务一个前缀。浏览器(以及测试)看到的是网关端口上的单一源;请求则悄悄散射到拥有该前缀的那个服务。这与生产环境同构——同样的路径前缀路由,只是坐在真正的边缘代理之后。
这里还藏着一处刻意的调优,正因为宿主网络才显得要紧。web 服务的 keep-alive 超时被特意设得比网关保持上游空闲连接的时间更长。否则 Node 的 standalone 服务器会先关掉连接池里的连接;nginx 并不知道,就会拿这条已死的 socket 去发下一个请求,撞上连接重置——又因为这些是它不会重试的 POST 请求,最终回给你一个 502。让服务器端连接的存活时间盖过代理的复用窗口,这个竞态就消失了。这种细节,只有当真实服务与真实代理对话时才会浮现,而这恰恰是这套 harness 的全部意义所在。
就绪检查是一段楼梯
就绪(readiness)在三个逐级升高的层面上被检查,一层比一层更严。
- 每服务的健康检查。 每个应用服务都声明一个 Docker healthcheck,按固定间隔请求自己的
/health端点,并为启动慢的服务留出较长的起始宽限期。 - 依赖门控。 网关对每个服务声明
depends_on,并要求其健康,于是 nginx 在每个后端都报告健康之前不会开始路由。没有 HTTP 面的 worker,则门控于其父服务健康,且只要求它已启动。 - 应用层验证。 网关起来之后,Playwright 的全局启动会再跑一遍自己的检查:它先等网关的聚合
/health应答,然后经由网关按各服务真实的 API 路径逐个探测,重试若干次,并把除 502/503 之外的任何响应都当作「活着」。这最后一层证明的不只是「容器在跑」,而是「网关确实能端到端够到它」——正是测试将要走的那条路。
全局启动:从一块干净的地面出发
在任何测试跑起来之前,全局启动会清掉上一轮可能遗留在共享基础设施里的状态——那些与被测代码毫无关系、却制造 flaky 的根源:
- Meilisearch 的搜索索引被清空,并且 harness 会轮询每个删除任务直到完成,好让没有一条陈旧文档能残存到某个搜索断言里。
- 后台任务队列(BullMQ,存在 Redis 里)被清空,好让没有一个残留任务在测试中途突然触发。
- Redis 里的限流计数器被抹掉——这是被明确允许的例外——好让新一轮的运行不会继承上一轮的节流。
这个顺序是刻意的:先验健康,再清数据面,最后才开始驱动。测试该遇到的,是一个既活着又空着的栈。
全局清理:schema 属于启动,不属于 setup
生命周期在收尾处有一个与之呼应的决定。表不是在运行前被删掉的——服务在启动时通过 Prisma push 自己创建自己的 schema。真正的清理放在全局 teardown:所有测试跑完之后,它清掉 Redis 里的测试键与队列、再次清空搜索索引,并把每个服务数据库里的所有表都删掉。schema 归服务的启动流程所有;harness 只保证事后数据库是空的。再加上每个服务按版本号 tee 出来的日志文件,一次跑完的运行既留下了自己的证据,也给下一轮留下了一块干净的地面。
为什么它读起来像一个系统
单看每一块都不稀奇——层叠的 compose 文件、宿主网络、一个按路径路由的 nginx、每容器的健康检查、一对 setup/teardown。让这套 harness 显得连贯的,是它们合成出的那一个保证:当一条测试的第一行开始执行时,整支舰队已经启动、各自健康、可以经由测试真正会用的那一个网关够到、并且已被清掉上一轮的数据。测试的作者得以把注意力放在「一个平台、一个地址」上,而底下的一切——散射、就绪的楼梯、清理——都已经被预先弄成了真。