Immich 测试体系全解:从服务端单测到 Docker 化的端到端测试实战
本篇指南基于 Immich 仓库官方的开发者测试文档 docs/docs/developer/testing.md 展开,系统讲解 Immich 服务端单元测试、端到端(e2e)测试与 Web UI 测试的完整运行方式,并结合 mise.toml 任务定义、vitest/Playwright 配置和 docker-compose 测试环境,深入剖析每个测试命令背后实际执行的构建链路与环境编排,帮助你在本地正确搭建、运行并理解 Immich 的全层测试体系。
一、测试分层总览:Immich 的三层测试结构
Immich 是一个高并发的自托管照片与视频管理方案,其质量保障由三层测试构成:
| 层级 | 位置 | 工具 | 覆盖对象 |
|---|---|---|---|
| 单元测试 | server | vitest | 核心服务、工具函数、纯逻辑 |
| 中型(集成)测试 | server/test/medium | vitest + 真实数据库 | 仓储层、数据库交互 |
| 端到端测试 | e2e | vitest / Playwright | 完整生产环境下的 API、CLI、Web |
三层测试通过 mise 任务统一调度,避免了手工拼装命令的混乱。
二、服务端单元测试
2.1 运行方式
根据官方文档,单元测试通过以下命令运行:
# 首次运行前只需执行一次,安装 server 依赖
mise //server:install
# 运行单元测试
mise //server:test
这两个命令对应 server/mise.toml 中定义的任务:
server:install实际执行pnpm install --filter immich --frozen-lockfile,即按锁文件安装 server 包(immich)的依赖;server:test实际执行vitest --config test/vitest.config.mjs,并显式把./node_modules/.bin加入 PATH。
2.2 单元测试到底跑了什么
从 server/test/vitest.config.mjs 可以看到单元测试的精确边界:
export default defineConfig({
test: {
name: 'server:unit',
root: serverRoot,
globals: true,
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
include: ['src/cores/**', 'src/services/**', 'src/utils/**', 'src/sql-tools/**'],
exclude: [
'src/services/*.spec.ts',
'src/services/api.service.ts',
'src/services/microservices.service.ts',
'src/services/index.ts',
],
},
server: { deps: { fallbackCJS: true } },
env: { TZ: 'UTC' },
},
plugins: [swc.vite(), tsconfigPaths()],
});
由此可以确认几个关键事实:
- 测试文件约定:单元测试即
src/**/*.spec.ts,与业务代码同目录存放(例如 server/src/validation.spec.ts); - 覆盖范围:覆盖率统计聚焦于
src/cores/、src/services/、src/utils/、src/sql-tools/四个目录,即业务核心逻辑,并排除了纯 API 转发与微服务调用等难以单测的胶水层; - 时区固定:
env: { TZ: 'UTC' }保证时间相关断言在任何开发机上结果一致; - 编译加速:使用
unplugin-swc替代 esbuild 做转译、vite-tsconfig-paths解析路径别名,是 NestJS + TypeScript 项目提速的常见组合。
2.3 中型测试与 CI 完整检查
server/mise.toml 中还定义了文档未直接提及但 CI 会执行的 test-medium 与 ci-unit 任务:
[tasks."test-medium"]
env._.path = "./node_modules/.bin"
run = "vitest --config test/vitest.config.medium.mjs"
[tasks.ci-unit]
run = [
{ task = ":install" },
{ task = "//:plugins" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test --run" },
]
其中 server/test/vitest.config.medium.mjs 将测试范围限定为 test/medium/**/*.spec.ts,并通过 globalSetup: ['test/medium/globalSetup.ts'] 在测试套件启动前完成环境准备(数据库等)。从 ci-unit 任务的编排看,CI 中一次完整的单测门禁顺序是:安装依赖 → 构建插件 → Prettier 格式检查 → ESLint → tsc --noEmit 类型检查 → vitest 运行。本地开发时若想对齐 CI 行为,可以直接执行 mise //server:ci-unit,而不必逐条手敲命令。
三、端到端(e2e)测试
3.1 运行步骤
官方文档给出的 e2e 完整流程分三步:
# 第 1 步:启动测试生产环境(交互式,会保持前台运行)
mise e2e
# 第 2 步:一次性环境准备(只需执行一次)
mise //e2e:ci-setup # 安装 e2e、SDK 和 CLI 依赖
mise //:open-api # 生成/同步 OpenAPI 类型与 SDK
# 第 3 步:在环境运行起来后执行测试
mise //e2e:test
3.2 每个命令背后的真实动作
mise e2e 启动了什么:对应根 mise.toml 中的 [tasks.e2e] 任务:
[tasks.e2e]
depends = "//:plugins"
dir = "e2e"
interactive = true
env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.yml up --remove-orphans"
depends_post = "//:e2e-down"
它先构建 Extism 插件(//:plugins),再进入 e2e 目录用 e2e/docker-compose.yml 拉起一套测试专用的生产环境,退出时自动执行 e2e-down 清理容器。该环境由四个服务组成:
| 服务 | 镜像/构建来源 | 端口 | 说明 |
|---|---|---|---|
immich-server |
server/Dockerfile | 2285 | 被测服务,IMMICH_ENV: testing,禁用机器学习,测试素材挂载到 /test-assets |
database |
自带 vchord/pgvector 扩展的 Postgres | 5435 → 5432 | fsync=off 提升测试吞吐,健康检查后才允许 server 启动 |
redis |
valkey:9 | — | 队列/缓存依赖 |
e2e-auth-server |
packages/e2e-auth-server/Dockerfile | 2286 | 本地 OAuth2 认证服务器,用于验证 OAuth 登录流程 |
几个值得注意的环境细节(均来自 e2e/docker-compose.yml):
IMMICH_MACHINE_LEARNING_ENABLED: 'false':e2e 环境不启动机器学习微服务,使测试不依赖 ML 模型;IMMICH_TELEMETRY_INCLUDE: all:测试环境开启全量遥测,便于断言遥测相关行为;./test-assets:/test-assets:挂载测试素材(图片、视频样本),供上传、缩略图、元数据提取等用例消费;cache_from指向远程构建缓存,加速 server 镜像构建。
mise //e2e:ci-setup 做了什么:对应 e2e/mise.toml:
[tasks.ci-setup]
run = [
{ task = "//:sdk:install" }, # pnpm --filter @immich/sdk install --frozen-lockfile
{ task = "//:sdk:build" }, # 构建 TypeScript SDK
{ task = "//packages/cli:install" },
{ task = "//packages/cli:build" },
{ task = ":install" }, # pnpm install --filter immich-e2e... --frozen-lockfile
]
这解释了文档中「安装 e2e、SDK 和 CLI 依赖」的含义:e2e 包(e2e/package.json)通过 workspace:* 直接依赖 @immich/sdk 与 @immich/cli,因此必须先构建这两个 workspace 包,e2e 才能以类型安全的方式调用完整 API 与命令行工具。
mise //:open-api 做了什么:对应根 mise.toml 的 [tasks.open-api],依次执行插件构建、server 安装与构建、sync-open-api(导出 OpenAPI 规范)、open-api-typescript(用 oazapfts 从 open-api/immich-openapi-specs.json 生成 TypeScript SDK 客户端 packages/sdk/src/fetch-client.ts)、open-api-dart(为移动端生成 Dart SDK)。e2e 测试依赖 SDK 客户端与 server 的类型同步,所以文档要求先执行这一步。
mise //e2e:test 做了什么:
[tasks.test]
depends = ["//e2e:build", "//e2e:ci-setup"]
env._.path = "./node_modules/.bin"
run = "vitest --run"
它自动确保镜像已构建、依赖已安装,然后运行 e2e/vitest.config.ts。
3.3 e2e 环境的智能自举
e2e/vitest.config.ts 有一个精巧的设计:vitest 启动时会先探测 http://127.0.0.1:2285/api/server/ping——
const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
const globalSetup: string[] = [];
if (!skipDockerSetup) {
try {
await fetch('http://127.0.0.1:2285/api/server/ping');
} catch {
globalSetup.push('src/docker-compose.ts');
}
}
- 如果你已经按文档手动执行过
mise e2e(ping 成功),就跳过重复启动; - 如果环境没起,则自动注册 e2e/src/docker-compose.ts 作为
globalSetup,由它执行docker compose up --build ...,并监听容器输出、直到出现Immich Microservices is running字样(60 秒超时)才放行测试; - 设置
VITEST_DISABLE_DOCKER_SETUP=true可以强制跳过自举,只针对已运行的环境跑测试。
该配置还定义了执行策略:include: ['src/specs/server/**/*.e2e-spec.ts'](只跑 server 目录下的 API/CLI 类用例)、testTimeout: 15_000、pool: 'threads' 且 maxWorkers: 1(API 测试串行执行以避免相互干扰)、retry: process.env.CI ? 4 : 0(CI 中对瞬时失败重试 4 次,本地不重试)。
3.4 覆盖场景与用例清单
官方文档列出的 e2e 检查范围包括:
- 认证与授权(Authentication and authorization)
- 查询参数、请求体与 URL 的校验(Query param, body, and url validation)
- 响应状态码(Response codes)
- 缩略图生成(Thumbnail generation)
- 元数据提取(Metadata extraction)
- 库扫描(Library scanning)
从 e2e/src/specs 的实际用例清单可以印证这些声明的落地位置:
e2e/src/specs/
├── server/
│ ├── api/ # album、api-key、asset、download、integrity、jobs、library、
│ │ # map、oauth、person、search、server、session、shared-link、
│ │ # stack、system-config、system-metadata、tag、trash、user、user-admin
│ ├── cli/ # login、server-info、upload、version
│ └── immich-admin/
├── web/ # asset-viewer(detail-panel、navbar、slideshow)、album、auth、
│ # duplicates、integrity、photo-viewer、shared-link、user-admin、websocket
└── maintenance/ # database-backups(server/web 各一份)、maintenance
其中 oauth.e2e-spec.ts 对应文档所说的认证与授权(配合 2286 端口的独立认证服务器),asset/library 相关用例覆盖上传、缩略图、元数据与库扫描,user/user-admin/session/api-key 等用例覆盖 URL 与参数校验及响应码断言。测试依赖 e2e/src/fixtures.ts、e2e/src/generators.ts(含 generators/ 下的随机数据生成器)与 e2e/src/ui(含 mock-network/ 网络拦截、specs/ UI 层用例)等工具模块构造请求与断言。
3.5 Web UI 测试(Playwright)
e2e/mise.toml 中还定义了文档未展开但同属 e2e 体系的 Playwright 任务:
[tasks."test-web"]
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
env._.path = "./node_modules/.bin"
run = "playwright test"
即 mise //e2e:test-web 会先安装浏览器(playwright install)再执行浏览器测试。从 e2e/playwright.config.ts 可以看到它划分为三个 project:
- web(
src/specs/web):走真实后端http://127.0.0.1:2285的 Web 页面流程,串行执行(workers: 1); - ui(
src/ui/specs):基于 e2e/src/ui/mock-network 做网络 mock 的纯 UI 测试,可完全并行; - maintenance(
src/specs/maintenance/web):维护模式(maintenance mode)下的页面行为。
配置中 webServer 会在测试前自动执行 docker compose up --build ... 拉起 2285 端口的环境并复用已存在的实例(reuseExistingServer: true),失败时自动截图、重试时录制 trace(trace: 'on-first-retry'),CI 环境下固定 4 个 worker 并重试 4 次。此外还支持通过 .env 中的 PLAYWRIGHT_BASE_URL、PLAYWRIGHT_SLOW_MO、PLAYWRIGHT_DISABLE_WEBSERVER 等变量调整行为。
四、环境管理与清理
所有测试环境都是 Docker Compose 项目,因此与 up 对应的 down 任务同样重要(定义在根 mise.toml):
| 命令 | 作用 |
|---|---|
mise e2e |
启动 e2e 生产测试环境(前台) |
mise //:e2e-down |
手动清理 e2e 环境 |
mise e2e-update |
以 --build -V 参数重建镜像后启动 |
mise //e2e:test |
运行 API/CLI 层 vitest e2e |
mise //e2e:test-web |
运行 Playwright Web UI 测试 |
由于 mise e2e 带有 depends_post = "//:e2e-down",按 Ctrl+C 退出前台任务时会自动清理容器,不会留下残留的 immich-e2e-* 容器与网络。
五、总结
Immich 的测试体系以 docs/docs/developer/testing.md 描述的三条命令为入口,背后是一整套由 mise 任务图编排的链路:server:test 用 vitest 以 UTC 时区、SWC 加速运行 src/**/*.spec.ts 单测并统计核心模块覆盖率;mise e2e 用 e2e/docker-compose.yml 拉起 server + Postgres + Redis + OAuth 认证服务器的完整测试环境;mise //e2e:ci-setup 与 mise //:open-api 保证 SDK、CLI 与 OpenAPI 类型先于测试就绪;mise //e2e:test 通过智能自举(ping 探测或自动 docker compose up)执行 API/CLI 层 21+ 个端点域与 4 个 CLI 命令的端到端断言,Playwright 则补齐 Web 页面层的验证。理解 mise.toml、server/mise.toml 与 e2e/mise.toml 中的任务定义,就能把文档中的每条命令对应到真实的构建与测试链路,为二次开发时的回归验证提供可靠基线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00