stack-orchestration-and-global-setup

全栈编排与全局启动

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 显得连贯的,是它们合成出的那一个保证:当一条测试的第一行开始执行时,整支舰队已经启动、各自健康、可以经由测试真正会用的那一个网关够到、并且已被清掉上一轮的数据。测试的作者得以把注意力放在「一个平台、一个地址」上,而底下的一切——散射、就绪的楼梯、清理——都已经被预先弄成了真。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →