LoopBack 4 MySQL仓库验收测试:本机、远程与Docker三路线

发布时间:2026/11/4 9:34:31

LoopBack 4 MySQL仓库验收测试:本机、远程与Docker三路线 接手一个用 LoopBack 4 写的服务端项目数据访问层往往是最容易出问题也最容易含糊带过的地方模型定义写得漂亮控制器也测了偏偏仓库Repository那段代码没人敢保证它在真实 MySQL 上跑起来是什么样。这篇内容就是我围绕LoopBack 4 MySQL 仓库验收测试这件事的一次完整梳理把本机实战、远程实例、Docker 容器三条运行路线全部跑一遍再把背后的原理和踩过的坑摊开讲。适合已经能写出 CRUD 仓库、但对“怎么证明它在真库上确实对”心里没底的中级开发者也适合想把数据库测试塞进流水线却一直被环境问题绊住的同学。读完你应该能自己搭出一套稳定、可并行、不污染数据、跑完就清干净的仓库层验收测试。1. 仓库层为什么必须拿真库做验收1.1 单元测试和验收测试的边界到底在哪先说清一个概念问题因为很多人把这两件事混着做最后两边都没做扎实。单元测试针对的是纯逻辑比如一个把 DTO 转成实体的小函数、一段计算分页偏移量的表达式这些东西不碰 IO跑起来毫秒级错了定位也快。而验收测试针对的是“协作”是仓库方法加上连接器、加上真实数据库引擎、加上 SQL 方言之后产生的那一坨行为。这坨行为里有一大半是你在 TypeScript 层面看不见的索引有没有生效、外键约束触发时的错误码是什么、自增主键在并发插入下会不会跳号、LIMIT/OFFSET拼出来的 SQL 在深分页时会不会扫全表。我在实际项目里的划分原则很粗暴只要一行代码最终会变成 SQL 发出去它就该有一条验收测试兜着。repository.create()、repository.find({where: ...})、repository.updateAll()这些都属于此列。反过来模型上的property装饰器校验、hasMany关系定义本身属于静态声明单元测试甚至类型检查就能覆盖不必浪费真库连接。这么划分的好处是测试金字塔不倒。验收测试天然慢一条用例几百毫秒到几秒你要是把校验逻辑也塞进去整个测试集跑一遍十分钟本地开发根本没人愿意跑最后就变成 CI 上红了一次没人管、两次没人管、三次直接注释掉。慢测试必须少而精这是纪律。1.2 内存连接器替代不了 MySQL 的五件事LoopBack 官方文档里演示仓库测试经常用内存连接器in-memory connector糊弄过去。我理解这个做法在教学场景下的便利但真拿它当验收手段等于自己骗自己。下面这五件事是我实际对比过的差异每一条都可能让线上翻车。差异点内存连接器表现真实 MySQL 表现唯一约束基本不生效或行为随意抛ER_DUP_ENTRY错误码 1062字段长度不校验随便存严格模式下直接报错非严格模式静默截断排序规则按 JS 字符串比较受collation影响大小写、重音都可能不同事务无真正隔离级别REPEATABLE READ下的快照读、间隙锁都会显现时间类型就是 JS Date受time_zone影响跨时区读写会漂移我印象最深的是第三条。有次做一个按名称模糊搜索的功能内存测试全绿上线后用户反馈“搜不出来”。查了半天才发现 MySQL 表用的排序规则对某些字符做了归一化处理LIKE %xxx%的匹配范围和 JS 的includes完全不是一回事。这种问题你在内存连接器上永远测不出来因为差异不在代码里在引擎里。还有第五条也特别隐蔽。MySQL 的DATETIME不带时区信息读写时驱动会按连接参数timezone做转换。内存连接器存的是原生Date对象压根没有这个转换环节。等你把数据搬到真库凌晨两点跑批的时候发现时间戳集体偏移八小时那才叫难受。我的建议很直接内存连接器只用来跑完全不涉及查询语义的纯逻辑测试比如仓库的钩子函数beforeSave之类里做的数据加工。凡是涉及 SQL 行为的一律上真库。2. 本机 MySQL 路线最快跑通的第一条路2.1 建库建账号与最小权限配置本机装一个 MySQL 是最省事的起点尤其对刚接触这块的人来说能直接在客户端里看表、看数据调试体感最好。但我强烈反对用root账号跑测试理由不是安全洁癖而是权限过大会掩盖问题。root有DROP DATABASE权限automigrate一不小心把库删了你都不知道普通账号没权限时反而会立刻报错帮你提前发现危险操作。建库和账号的 SQL 大概是这样CREATE DATABASE IF NOT EXISTS lb4_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS lb4_test% IDENTIFIED BY lb4_pass; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, REFERENCES ON lb4_test.* TO lb4_test%; FLUSH PRIVILEGES;注意我给的权限里有CREATE、DROP、INDEX、ALTER这几个是必须的因为测试要反复建表删表。但故意没给CREATE USER、GRANT OPTION、SUPER也没有*.*这种全局授权。REFERENCES这个权限容易被漏掉一旦模型里定义了外键关系没有它建表会失败报的错还特别晦涩。字符集一定要显式指定utf8mb4。我见过好几个项目建库时图省事用了默认值后来存表情符号或者某些中日韩生僻字直接乱码回头改表、改列、改排序规则数据迁移能把人折腾一周。测试库和正式库的字符集必须完全一致否则测试通过不代表线上能存。还有个小细节utf8mb4_unicode_ci和utf8mb4_0900_ai_ciMySQL 8 默认在比较行为上有差异。如果测试库和线上库排序规则不同某些边界用例大小写敏感、重音字符的结论会不一致。我的做法是以线上库为基准测试库一模一样照抄别让引擎版本自己决定。2.2 测试专用数据源怎么和业务数据源解耦LoopBack 4 的数据源DataSource是通过依赖注入管理的业务代码里的仓库注入的是应用启动时注册的那个数据源。如果测试直接复用这个数据源问题就来了测试环境需要不同的数据库名、不同的连接池大小、甚至不同主机你不能把这些测试专用配置写进生产配置文件里。我的做法是让数据源配置优先从环境变量读代码里只留兜底默认值// src/datasources/mysql.datasource.ts import {inject, lifeCycleObserver, LifeCycleObserver} from loopback/core; import {juggler} from loopback/repository; const config { name: mysql, connector: mysql, host: process.env.DB_HOST ?? 127.0.0.1, port: Number(process.env.DB_PORT ?? 3306), user: process.env.DB_USER ?? lb4_test, password: process.env.DB_PASSWORD ?? lb4_pass, database: process.env.DB_NAME ?? lb4_test, connectionLimit: 10, connectTimeout: 10000, charset: utf8mb4, timezone: 08:00, }; lifeCycleObserver(datasource) export class MysqlDataSource extends juggler.DataSource implements LifeCycleObserver { static dataSourceName mysql; static readonly defaultConfig config; constructor( inject(datasources.config.mysql, {optional: true}) dsConfig: object config, ) { super(dsConfig); } }然后在测试文件里我不走依赖注入直接new一个实例出来把配置覆盖掉import {MysqlDataSource} from ../../src/datasources; import {UserRepository} from ../../src/repositories; const ds new MysqlDataSource({ ...MysqlDataSource.defaultConfig, database: process.env.DB_NAME ?? lb4_test, connectionLimit: 4, });这里的关键判断是仓库测试不一定要走完整的 Application 启动流程。很多人习惯先new Application()再app.boot()然后从容器里app.getRepository(...)。这么写在需要测 HTTP 层的时候是必须的但只测仓库的话太重了。每个测试文件启动一次完整应用几秒就没了十个文件就是几十秒。直接new数据源、new仓库依赖关系清晰启动开销几乎为零。2.3 automigrate 的触发时机与代价建表这件事在真实项目里是迁移脚本干的但在测试里通常走automigrate()。这个方法的实现是先DROP TABLE IF EXISTS再按模型定义CREATE TABLE顺带把所有索引、外键都建出来。干净、确定、可重复非常适合测试。before(async () { await ds.connect(); await ds.automigrate([user, order, order_item]); });传数组的时候LoopBack 会自动处理模型之间的外键依赖顺序你不需要自己排。这点做得挺贴心。但有两个代价必须认清。第一个代价是它只认模型不认历史迁移。如果线上表结构经历过多次 ALTER和模型定义已经产生了偏差常见于列长度改过、索引加过又删过automigrate建出来的表跟线上不是同一个东西测试结论的可信度就要打折。我的应对方式是维护一份对照检查定期拿线上SHOW CREATE TABLE的结果和自己automigrate出来的结果比一比发现差异就去补迁移脚本或者修模型定义。这个动作很枯燥但它能筛掉一类最隐蔽的“测试全绿、线上报错”。第二个代价是它真的很慢。每次automigrate都是若干次 DDLMySQL 的 DDL 不像 DML 那么轻量。如果你的测试文件有二十个每个before都跑一遍automigrate光建表就能耗掉半分钟。我的优化做法是只在第一个测试文件里做一次automigrate后续文件复用同一套表靠deleteAll()清数据。// test/setup/migrate.ts 在全局 setup 里跑一次 export default async function globalSetup() { const ds new MysqlDataSource(MysqlDataSource.defaultConfig); await ds.connect(); await ds.automigrate([user, order, order_item]); await ds.disconnect(); }对应地把 jest 配置加上globalSetup各测试文件里就不要automigrate了只留清数据。这一改动在我手上的项目里把全量测试从 90 秒压到了 35 秒左右收益相当明显。提醒一句automigrate千万别在有数据的库上跑它真的是先删表。生产环境的迁移请用专门的迁移工具automigrate只配出现在测试里而且建议在环境变量里加个断言比如if (process.env.NODE_ENV production) throw new Error(...)防止哪天脚本被人误执行。3. 远程实例路线连接参数、超时与并行隔离3.1 连接池上限该怎么算才不炸本机跑顺了之后下一步往往是接入共享的远程 MySQL 实例不管是自建还是托管服务。这条路最大的变化是你不独占数据库连接数、超时、隔离都成了需要精打细算的资源。先说连接池。connectionLimit这个参数设多少合适不能拍脑袋。有个可以照着算的公式峰值连接数 ≈ 并行测试进程数 × 每个进程的数据源实例数 × connectionLimit举例假设你用 jest 默认的并行策略8 个 worker 同时跑每个 worker 里有 3 个测试文件各自new了一个数据源虽然理论上一个 worker 里文件是串行执行的但如果你没显式断开连接连接会一直挂着connectionLimit设 10那理论上峰值就是 240 个连接。而 MySQL 默认max_connections是 151直接打爆报错是ER_CON_COUNT_ERROR错误码 1040提示信息Too many connections。我的实际配置是8 个 worker、每个 worker 最多 1 个活跃数据源、connectionLimit给 4峰值 32 个连接给其他应用留足余量。测试场景下的并发度本来就不高4 个连接完全够用反而能更快暴露“连接泄露”问题——如果你的代码里有查询没释放连接小池子会立刻堵死报错大池子则会把故障拖到运行时。const ds new MysqlDataSource({ ...MysqlDataSource.defaultConfig, connectionLimit: 4, connectTimeout: 8000, acquireTimeout: 8000, });connectTimeout和acquireTimeout这两个也值得单独说。前者是建立 TCP 连接的超时后者是从池子里取连接的超时。默认值一个是 10 秒另一个可能根本没有限制。在 CI 环境里如果数据库容器还没起来测试就会卡在那个“取连接”上直到超时日志里啥也看不出来只看到一个莫名其妙的 30 秒超时。所以我会把这两个都压到 8 秒并且保证在before里显式await ds.connect()让失败点明确。3.2 用独立 schema 隔离解决并行冲突并行测试最容易踩的坑是数据互相干扰。A 文件插了一条email aexample.comB 文件也插同一条唯一索引一冲突两个文件的结论都不可信了。deleteAll()只能解决顺序冲突解决不了时间重叠的冲突。最省心的隔离方案是给每个 worker 分配独立的库。思路是全局 setup 阶段先建好 N 个库测试运行时按JEST_WORKER_ID选库// test/global-setup.ts import mysql from mysql; export default async function globalSetup() { const workers Number(process.env.LB4_TEST_WORKERS ?? 4); const conn mysql.createConnection({ host: process.env.DB_HOST ?? 127.0.0.1, port: Number(process.env.DB_PORT ?? 3306), user: process.env.DB_ROOT_USER ?? root, password: process.env.DB_ROOT_PASSWORD ?? , multipleStatements: true, }); await new Promisevoid((resolve, reject) { conn.connect(err (err ? reject(err) : resolve())); }); for (let i 1; i workers; i) { const name lb4_test_${i}; await new Promisevoid((resolve, reject) { conn.query( CREATE DATABASE IF NOT EXISTS \${name}\ CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, err (err ? reject(err) : resolve()), ); }); } conn.end(); }测试文件里取库名const dbName lb4_test_${process.env.JEST_WORKER_ID ?? 0};注意全局 setup 用的是高权限账号因为要建库而测试运行用低权限账号只能操作自己那个库。这个权限分层很关键它同时解决了隔离和安全两个问题。JEST_WORKER_ID从 1 开始编号不是从 0。我第一次写的时候按 0 开始建库结果所有 worker 挤在同一个库上冲突照旧还多花了半小时才反应过来。这种小坑看文档能躲但很多人不看。3.3 时区、字符集与排序规则三件套远程实例最让人抓狂的三类差异我统一叫它们“三件套”。这三样只要有一项对不上测试结论和线上行为就可能分叉。时区。MySQL 连接参数里的timezone决定驱动如何把 JS 的Date转成字符串、又如何把字符串转回Date。常见写法是08:00或者ZUTC。如果测试库设成东八区、线上库设成 UTC同一个new Date()存进去再读出来差八小时。我的习惯是连接参数和 MySQL 服务端time_zone变量都显式设成同一值绝不依赖默认。字符集。除了库、表级别的utf8mb4连接级别也要设。驱动参数里那个charset就是干这个的别省。我见过连接字符集用了latin1、写中文全是问号的案例排查了半天还以为是代码编码问题。排序规则。影响ORDER BY、WHERE name ?、LIKE。utf8mb4_general_ci和utf8mb4_unicode_ci在重音、特殊字符上行为不同_ci结尾不区分大小写_cs或_bin区分。测试库必须和线上库保持同一排序规则否则会出现“测试里where name ABC能查到abc线上查不到”这种诡异现象。-- 自检当前会话的实际设置 SELECT character_set_connection, collation_connection, time_zone, sql_mode;这条查询我建议你在写第一行测试代码前就跑一次把输出记下来然后和线上库对比。差在哪里一目了然。4. Docker 路线一条命令起库并接入流水线4.1 容器启动参数的正确姿势Docker 跑 MySQL 的价值在于环境完全可复现。本机装 MySQL 会因为版本、配置、残留数据各有差异而镜像拉下来就是同一个东西团队里谁跑结果都一样流水线上更是一模一样。最基础的启动命令docker run -d \ --name lb4-mysql-test \ -e MYSQL_ROOT_PASSWORDrootpass \ -e MYSQL_DATABASElb4_test \ -e MYSQL_USERlb4_test \ -e MYSQL_PASSWORDlb4_pass \ -p 3306:3306 \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci这里有几个参数必须解释清楚为什么加。MYSQL_DATABASE会在容器初始化时自动建库省掉你手动敲 SQLMYSQL_USER和MYSQL_PASSWORD会自动建一个对该库有完整权限的普通账号正好满足前面说的最小权限原则。关键在最后两行--character-set-server和--collation-server。它们通过command传给 mysqld决定服务端默认字符集。不加的话容器用的就是镜像默认值某些版本默认可能就是utf8mb4_0900_ai_ci和服务端环境不一定对得上。显式声明心里踏实。还有一个非常出名的兼容性坑MySQL 8 默认认证插件是caching_sha2_password而一些老的 MySQL 驱动对它的支持不完整连接时会报ER_NOT_SUPPORTED_AUTH_MODE。如果你用的连接器版本偏旧可以加一条启动参数--default-authentication-pluginmysql_native_password这条是权宜之计长期还是建议把驱动升级到支持新认证方式。但在赶进度的时候它能让你五分钟内跑起来先通后优我觉得没问题。4.2 等库就绪别用 sleep 赌运气容器docker run -d返回的时候MySQL 往往还没初始化完。它的启动过程是拉起进程 → 初始化数据目录 → 建库建用户 → 开放端口监听。这个序列在慢机器上可能要二三十秒。很多脚本用sleep 20硬等结果 CI 机器一卡就失败重跑一次又过了典型的 flaky。正确的做法是探活。MySQL 镜像里自带mysqladmin用它 ping 一下最直接until docker exec lb4-mysql-test \ mysqladmin ping -h 127.0.0.1 -u root -prootpass --silent; do echo 等待 MySQL 就绪... sleep 2 done注意-h 127.0.0.1不能省。容器里的 socket 连接和 TCP 连接的可用时机不完全同步用 TCP 探活才代表外部能连上。这个细节坑过不少人报错是Cant connect to local MySQL server through socket但端口其实还没监听。用 compose 的话直接上healthcheck更优雅version: 3.8 services: mysql: image: mysql:8.0 command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: lb4_test MYSQL_USER: lb4_test MYSQL_PASSWORD: lb4_pass ports: - 3306:3306 healthcheck: test: - CMD - mysqladmin - ping - -h - 127.0.0.1 - -u - root - -prootpass interval: 5s timeout: 5s retries: 20 start_period: 20sstart_period这个字段值得单拎出来说。它表示“容器启动后这段时间内失败不计入 retries”。不给的话MySQL 初始化阶段的前几次探测失败会消耗重试次数导致明明还在初始化就被判死。设成 20 秒是个比较稳的经验值。4.3 把它塞进测试脚本和流水线本地开发和流水线上跑测试前后的动作应该统一成两条 npm 脚本别让人记一堆 docker 命令{ scripts: { db:up: docker compose -f docker-compose.test.yml up -d --wait, db:down: docker compose -f docker-compose.test.yml down -v, test:db: npm run db:up npm test npm run db:down } }--wait是 compose 的一个好用选项它会一直等到所有带healthcheck的服务变成 healthy 才返回等于帮你把探活逻辑内置了。down -v里的-v是删数据卷保证下次启动是干净状态——测试要的就是每次都从零开始不要残留。在流水线里如果你的 CI 平台支持 service 容器把上面那份 compose 里的镜像和参数搬过去就行如果不支持就用docker run脚本。两种情况我都跑过关键是把环境准备和测试执行拆成两个步骤环境准备失败和测试失败在日志里要能一眼分开否则出问题的时候根本定位不到是哪一层挂了。一个实战细节CI 里跑完测试记得抓容器日志。如果测试失败原因不明docker logs lb4-mysql-test里往往能看到死锁、连接拒绝、认证失败这些引擎侧的信息比测试报告详细得多。我习惯在after_script或等价的收尾阶段里无条件 dump 一下失败时能直接看。5. 用例编写实录造数、断言与清理5.1 生命周期钩子的执行顺序别搞反一段可靠的仓库测试骨架在于钩子的顺序。我常用的结构是before建连接、after断连接、beforeEach清数据。顺序反了就是灾难连接没建好就清数据会报错连接断太早后面用例全挂。import {expect} from loopback/testlab; import {MysqlDataSource} from ../../src/datasources; import {UserRepository} from ../../src/repositories; describe(UserRepository 验收测试, () { let ds: MysqlDataSource; let repo: UserRepository; before(async () { ds new MysqlDataSource({ ...MysqlDataSource.defaultConfig, database: lb4_test_${process.env.JEST_WORKER_ID ?? 1}, connectionLimit: 4, }); await ds.connect(); repo new UserRepository(ds); }); after(async () { await ds.disconnect(); }); beforeEach(async () { await repo.deleteAll(); }); it(创建后能按主键读回, async () { const created await repo.create({name: A同学, email: aexample.com}); const found await repo.findById(created.id); expect(found.email).to.equal(aexample.com); }); });ds.connect()这行其实是可以省的连接器是懒加载的第一次查询时才真正连。但显式写出来有个巨大好处连接失败会在before阶段立刻暴露报错信息指向明确的初始化步骤而不是淹没在第一条用例里。这一点在远程实例和容器环境下尤其重要因为连接失败是最高频的故障。after里的disconnect()千万别忘。忘了的后果是 jest 进程挂住不退出你会看到测试结果都出来了但命令行就是不返回必须--forceExit。而--forceExit又会掩盖真正的资源泄露问题属于治标不治本。老老实实断开让它自然退出。5.2 约束类用例怎么写才不脆验收测试里最有价值的是约束验证最容易写崩的也是它。崩的原因通常是断言方式不对——直接断言错误信息文本。// 脆弱写法MySQL 版本一升级错误文案变了就挂 await expect( repo.create({name: B同学, email: dupexample.com}), ).to.be.rejectedWith(Duplicate entry);MySQL 的错误文案在不同版本、不同语言包里都可能变。稳妥的写法是断言错误码it(重复邮箱触发唯一约束, async () { await repo.create({name: B同学, email: dupexample.com}); try { await repo.create({name: C同学, email: dupexample.com}); throw new Error(预期应该抛出唯一约束错误但没有); } catch (err: any) { expect(err.code).to.equal(ER_DUP_ENTRY); } });错误码ER_DUP_ENTRY是 MySQL 的稳定契约对应数字 1062跨版本不会变。同样的思路也适用于外键约束ER_NO_REFERENCED_ROW_2、非空约束ER_BAD_NULL_ERROR、字段超长ER_DATA_TOO_LONG。这里有个容易被忽略的前提只有开启了严格模式字段超长才会报错。MySQL 5.7 之后默认开启STRICT_TRANS_TABLES但如果你在容器启动参数或配置文件里改过sql_mode超长字段会静默截断而不是报错那你的用例就会得到“插入成功”的结果断言直接失效。所以严格模式这件事也建议在测试初始化时断言一下const [rows] await ds.execute(SELECT sql_mode AS mode); expect(rows[0].mode).to.contain(STRICT_TRANS_TABLES);这行代码看着多余但它能防止一类“测试在某个环境里悄悄失效”的事故。测试的可信度建立在环境确定的前提上环境本身也该被测试。5.3 事务用例与回滚验证事务行为是内存连接器完全模拟不了的部分也是我认为最该上真库的一类用例。写事务测试有个常见误区只在仓库方法上传transaction参数却忘了验证提交和回滚的实际效果。it(事务回滚后数据不落库, async () { const tx await ds.beginTransaction(); try { await repo.create( {name: D同学, email: rollbackexample.com}, {transaction: tx}, ); await tx.rollback(); } catch (err) { await tx.rollback(); throw err; } const result await repo.count(); expect(result.count).to.equal(0); });count()这里读的是新连接不是事务里的那个所以它看不到未提交的数据。这个断言才真正验证了“回滚生效”比断言方法返回值有意义得多。还有一个更容易被忽视的场景跨仓库事务。比如创建订单和扣减库存要在一个事务里如果两个仓库用了不同的数据源实例事务就串不起来回滚只能回滚一半。测试应该覆盖这条路径it(跨仓库事务回滚时两个仓库都不留数据, async () { const tx await ds.beginTransaction(); try { await orderRepo.create({userId: 1, amount: 100}, {transaction: tx}); await stockRepo.updateAll({userId: 1}, {quantity: 99}, {transaction: tx}); throw new Error(模拟业务异常); } catch (err) { await tx.rollback(); } expect((await orderRepo.count()).count).to.equal(0); expect((await stockRepo.findById(1)).quantity).to.equal(100); });这类用例我在每个用到事务的项目里都会写至少一条。它可以挡住一类非常典型的错误某个仓库方法内部自己开了新连接把事务切断了。这种 bug 靠代码审查很难发现靠测试一跑就露馅。清理数据时如果表之间有外键deleteAll()的顺序会导致ER_ROW_IS_REFERENCED_2。两个解法按依赖倒序清先删子表再删父表或者临时关掉外键检查。我更推荐前者虽然啰嗦一点但不会因为忘了恢复检查而污染后续用例。6. 排错速查从连接拒绝到死锁重试6.1 报错对照表这张表是我这两年积累下来的遇到报错先对一遍大部分情况能直接定位报错信息 / 错误码常见成因处理方向ECONNREFUSED端口没监听、容器未启动完成加探活确认-h 127.0.0.1ER_ACCESS_DENIED_ERROR账号密码错、Host 不匹配检查user%授权与实际连接源ER_NOT_SUPPORTED_AUTH_MODE驱动不支持新认证插件升级驱动或改用旧认证插件ER_CON_COUNT_ERROR(1040)连接池开太大、连接泄漏降connectionLimit检查是否漏disconnectER_DUP_ENTRY(1062)唯一索引冲突清数据或改用唯一值造数ER_NO_REFERENCED_ROW_2外键指向的主表记录不存在调整造数顺序ER_LOCK_DEADLOCK(1213)并行用例交叉更新同一批行调整更新顺序或加重试ER_TRUNCATED_WRONG_VALUE字符集或时区不一致对齐三件套ER_DATA_TOO_LONG字段长度不够检查模型列定义与迁移是否一致ER_LOCK_DEADLOCK这条我想多说两句。它的本质是两条事务以不同顺序锁同一批行MySQL 检测到环路后主动杀掉其中一个。在并行测试里这往往不是代码 bug而是用例并发度过高导致的假阳性。我的处理策略是给测试用的写操作加一层小重试捕获 1213 错误后等 50 到 200 毫秒重试最多三次。生产代码里当然要做更严谨的设计但测试代码里这个策略能显著降低 flaky 率。当然前提是你确认死锁不是真 bug。6.2 几个反直觉的实操经验别断言自增 ID 的连续值。很多人喜欢写expect(created.id).to.equal(1)。这条断言在不同环境下随时会挂deleteAll()不会重置自增计数器并行测试插入顺序不确定AUTO_INCREMENT在有回滚的事务里还会跳号。正确做法只断言 ID 存在且为正需要用具体 ID 时从返回值里取。updateAll返回的是数量不是记录。这个在写断言时特别容易犯迷糊await repo.updateAll({name: x}, {status: 1})返回{count: 3}不是三个实体。想验证更新内容得再查一次。这不是 LoopBack 的怪癖是 SQLUPDATE的语义但第一次碰到的人基本都会写错。连接器版本和数据库版本的组合要锁死。我在一个项目里遇到过mysql连接器某版本对JSON类型字段的处理有 bugautomigrate建出来的列类型和预期不符。后来在package.json里把连接器版本钉死不用^并加了一条测试断言SHOW COLUMNS的结果才算稳住。环境不确定是 flaky 测试最大的来源能钉死的都钉死。慢查询日志是最便宜的诊断工具。验收测试失败但错误信息看不懂的时候去 MySQL 开一下慢查询日志或者临时把long_query_time设成 0 抓全部 SQL看看仓库到底发出了什么语句比读代码猜要快十倍。我现在的习惯是每个新项目第一次跑验收测试时都开着这个日志看一遍很多“为什么查不到”的问题当场就明了。清数据比造数据更容易出问题。造数据失败会立刻报错清数据失败往往是静默污染——某条记录没删掉下一条用例的唯一索引就冲突了报错还指向错误的用例。所以我的beforeEach清数据一定要把表列全而且宁可多删几个表也别漏。有一个小技巧是启动时先查一遍information_schema.tables拿到所有表名按外键依赖排序后逐个TRUNCATE比手写一长串deleteAll()更不容易漏。我个人的体会是仓库层验收测试的价值不在于“证明代码是对的”而在于把数据库当成一个会说话的对手。它有自己的规则、自己的边界、自己的脾气你在代码层面做的每一个假设都得拿到它面前验证一遍。上面这套本机、远程、Docker 三条路线本质上是在应对不同场景下的确定性问题本机图快远程图真实Docker 图可复现。三者不冲突我的项目里通常是本机开发时用本机库、提交流水线用容器库、偶尔需要复现线上问题连远程实例用环境变量切换配置一套代码通吃。最后再分享一个小习惯每次发布前我会把验收测试对着线上库的只读副本跑一遍。不是全跑只跑那些涉及查询语义和约束的用例。这一步花不了几分钟但它抓出过两次“模型定义和线上表结构已经不一致”的问题那种问题如果等到用户反馈再发现代价就大得多。
延伸阅读

更多相关文章

2026/11/4 9:34:31

Obsidian+LLM自动更新个人知识库:增量去重与巡检实践

我一直觉得,个人知识库里最难的不是"记下来",而是"记下来之后还有人管"。我的笔记库里躺着几千条剪藏、摘录和半截想法,真正被复用的不到百分之五。后来我把这套流程改造成"机器负责整理、人负责判断"的结构&a…

2026/11/4 9:34:31

AI Agent知识获取管道:RAG不是插件而是神经反射弧

1. 这不是“加个知识库”那么简单:RAG 在 AI Agent 架构里的真实定位你肯定见过这样的宣传:“给你的 AI Agent 接个 RAG,秒变行业专家!”——听起来像给手机装个新 App,点几下就完事。但我在过去三年里亲手搭过 17 个不…

2026/11/4 9:34:31

LangGraph 从入门到实战:StateGraph 与 Agent 工具调用循环

1. 从零理解 LangGraph:它到底解决什么问题 1.1 为什么有了 LangChain 还需要 LangGraph 很多人第一次接触 LangGraph 都会问同一个问题:我已经在用 LangChain 了,为什么还要多学一个框架?这个问题我当初也纠结过,后来…

2026/11/4 12:20:57

WDCNN轴承故障诊断:小样本强噪声下的双支路特征解耦方法

简介:本资源是一套基于WDCNN(加权深度卷积神经网络)的轴承故障诊断完整实现方案,面向机械故障诊断、工业智能运维及深度学习入门到进阶的学习者,解决旋转机械关键部件——轴承在不同故障类型(内圈、外圈、滚…

2026/11/4 12:20:57

禅道角色权限与流程避坑指南:从登录到闭环的实战操作地图

简介:本资源为《禅道使用手册(完整版)》,面向项目管理初学者、敏捷开发团队成员及中小企业项目负责人,系统解决项目协作、需求跟踪、BUG闭环与角色权限配置等核心问题。手册覆盖从基础登录、用户角色划分、最简工作流到…

2026/11/4 12:20:57

Unity WebGL中文输入法实战:候选框跟随光标与全屏适配

简介:这份资源面向需要在 Unity WebGL 项目中实现中文文本输入的开发者,重点解决浏览器环境下输入法无法正常唤起、输入框不跟随光标以及全屏模式下定位错乱等常见问题。压缩包共 19 个文件,约 17.4MB,包含 html 入口页、css 样式…

2026/11/4 12:20:57

Unity WebGL中文输入实战:JS桥接与全屏适配方案

简介:这份资源面向需要在 Unity WebGL 项目中实现中文文本输入的开发者,重点解决浏览器环境下输入法无法唤起、输入框不跟随光标以及全屏模式下定位错乱等常见问题。压缩包共 19 个文件,约 17.4MB,包含 html 入口页、css 样式、js…

2026/11/4 12:20:57

DirectShow Win10音视频采集实战:低延迟硬件对接方案

简介:本资源是面向Windows平台C多媒体开发者的DirectShow实战工具包,专为在Win10环境下快速上手和稳定运行DirectShow框架而整理优化。内容涵盖基础过滤器模型解析、COM接口调用示例、Filter Graph构建逻辑及GraphEdit调试方法,适用于视频播放…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 0:03:20

AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年…

2026/11/4 0:03:20

从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你…

2026/11/4 0:03:20

32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑