触发器测试必须用BEGIN/ROLLBACK控制事务可见性,否则无法验证拦截效果或中间态修改;需覆盖字段组合边界、捕获RAISE/SIGNAL异常、避免DDL耦合,并验证日志类触发器是否漏发。

触发器测试不是“跑一遍SQL看结果”,而是要控制事务边界、捕获异常、验证中间态——否则你根本不知道它是不是真执行了,还是被 silently ignored 了。
用 BEGIN / ROLLBACK 控制事务可见性
触发器在事务内执行,但默认测试框架(如 pytest + psycopg2 或 JUnit + JDBC)往往自动 commit,导致你 INSERT 后查不到拦截效果,UPDATE 后看不到 NEW 值是否被改写。
- 所有测试函数开头必须显式
BEGIN,结尾统一ROLLBACK,不依赖任何框架的 auto-commit 清理机制 - PostgreSQL 支持
SAVEPOINT,可嵌套验证多步链式触发;SQLite 也支持,但 MySQL 5.7 及更早版本只能靠完整事务回滚,测AFTER UPDATE再INSERT链式逻辑时容易串扰 - 别在测试里用
SELECT * FROM table看最终状态就完事——要验证 BEFORE 触发器是否修改了NEW.amount,得在RETURN NEW后立刻SELECT amount FROM table WHERE id = ?
覆盖触发器真正依赖的字段组合边界
边界不是“空字符串”或“超长文本”,而是触发器逻辑中显式引用的字段值组合。比如一个 BEFORE UPDATE 触发器只在 status = 'pending' 且 amount > 100 时重设 priority,那必须测:
-
status不变('pending' → 'pending'),amount从 90 → 110:应触发 -
status变化但不满足条件('draft' → 'pending',amount = 50):不应触发 -
status从'pending' → 'done',amount也变:触发器是否错误读取了OLD.amount?需检查日志或目标字段 - INSERT 时某列为
DEFAULT(如created_at TIMESTAMP DEFAULT NOW()),触发器里读NEW.created_at可能是NULL(MySQL)或已填充(PostgreSQL),行为不一致
处理 RAISE EXCEPTION 和批量操作的特殊逻辑
触发器里 RAISE EXCEPTION(PostgreSQL)或 SIGNAL SQLSTATE(MySQL 8.0+)会让整个事务中断,后续断言直接跳过——你得主动捕获,而不是让它崩掉测试流程。
- PostgreSQL 测试必须包在
BEGIN ... EXCEPTION WHEN others THEN ... END块里,再用GET STACKED DIAGNOSTICS提取错误信息做断言 - MySQL 中不能用存储过程包装
SIGNAL来“软捕获”,只能靠应用层连接重试或日志表 fallback;建议把校验类逻辑移到 BEFORE 触发器 +SET NEW.col = ...,而非硬抛错 -
INSERT INTO ... SELECT是单语句多行,但行级触发器仍按行触发,OLD/NEW永远是单行——别指望在触发器里拿到“本次插入共 100 行”的上下文,要用语句级触发器(MySQL 不支持)或改用应用层批处理
避免测试与 DDL 强耦合
在测试代码里拼 CREATE TRIGGER ... 字符串,等于把数据库 schema 当业务逻辑硬编码。CI 失败、本地环境不一致、重命名触发器后全挂——这些全是自找的。
- 测试前确保触发器已存在:CI 走 migration 脚本加载,本地用固定
init.sql初始化,测试只负责输入输出验证 - 用
CREATE TEMP TABLE或独立 schema(如test_前缀)隔离测试数据,防止多个测试用例的触发器互相污染 - 不要在测试里
DROP TRIGGER再重建——PostgreSQL 不支持CREATE OR REPLACE TRIGGER,强行删重建会引发竞态,尤其并发测试时
最常被忽略的一点:日志类触发器(比如写 audit_log)必须验证“是否漏发”,而不是“发了之后内容对不对”。一次 INSERT 没触发日志,可能只是事务没提交,也可能触发器根本没绑定到正确事件——得先确认 pg_trigger 或 information_schema.triggers 里它确实存在且 evntman 字段匹配。

















