FluentValidation 测试断言报 ArgumentNullException,检查失败项的属性名
FluentValidation 12.1.0 的表达式测试断言会把每条失败项的 PropertyName 送入正则归一化,null 值可能让断言自身崩溃。12.2.0 已加保护;平面属性的临时字符串断言不等于列表路径归一化。
验证器已经返回失败结果,测试调用 ShouldHaveValidationErrorFor(x => x.CustomerName) 却没有给出预期的断言结果,而是抛出 ArgumentNullException。此时先看堆栈是否指向 TestValidationResult.NormalizePropertyName,并检查失败项中是否有 PropertyName == null。
这个条件可以出现在模型级或自定义验证结果中,但不能反过来认定任何 RuleFor(x => x) 都必定产生 null;实际名称还受规则构造和解析方式影响。真正触发这条路径的是结果集合里存在 null 属性名,而表达式断言要遍历并归一化它。
失败可能发生在匹配目标属性之前
正式 12.1.0 的 NormalizePropertyName 直接调用 Regex.Replace(propertyName, ...) 去掉集合索引。null 传入正则操作时抛异常,因此即使正在检查的是另一个完全正常的属性,断言也可能先在无关失败项上终止。
下面是用于说明测试结果入口的 C# 片段,需要 FluentValidation.Results 和 FluentValidation.TestHelper;它显式构造一条 null 属性名失败,不假设所有模型规则都会如此:
var validation = new ValidationResult(new[]
{
new ValidationFailure(null!, "Order is invalid."),
new ValidationFailure("CustomerName", "Name is required.")
});
var result = new TestValidationResult<Order>(validation);
result.ShouldHaveValidationErrorFor(x => x.CustomerName);
Order 是测试项目已有的模型,包含 CustomerName 属性。若生产结果的名称是空字符串而不是 null,这个具体的正则空参数故障就不成立;不要仅根据“模型级规则”四个字删除业务校验。
12.2.0 已在归一化入口保留 null
正式 12.2.0 增加了 null 判断,让它不再进入 Regex.Replace;其余属性名称仍按原规则归一化。该版本的发行说明也明确列出 TestValidationResult 对 null PropertyName 的修正。
使用 12.x 的项目可采用 12.2.0 或包含同项修正的兼容版本,核对测试项目实际加载的包。12.x 的最低平台为 .NET 8;更旧目标框架不能只替换一个版本号,还需评估对应依赖和迁移范围。无需为修复测试辅助器而删掉生产验证规则,或让所有模型错误改挂在任意字段上。
旧版可以对平面属性使用精确字符串入口
对名称明确的平面属性,字符串重载不会走相同的正则归一化分支:
result.ShouldHaveValidationErrorFor("CustomerName");
这项替代要求失败项里的真实属性名就是 CustomerName。自定义 PropertyNameResolver、OverridePropertyName 或复杂对象路径存在时,应对照最终结果里的完整名称,不能把 C# 成员名当成永远正确的字符串。
集合也要另行核对:表达式重载原本会处理索引,而字符串重载按传入路径匹配。原错误是 Items[0].Name 时,机械改成 Items.Name 可能使测试从框架异常变成找不到错误。不要为避免 null 就先过滤掉全部模型级错误,再以一个不完整的结果集合证明验证通过。
复查时保留一条 null 属性名失败和一条目标属性失败,再覆盖目标属性没有错误的情况。预期是前者不会让辅助器崩溃,真正缺失的目标错误仍应使断言失败;其他失败项的信息也不应被清除。测试恢复运行后,仍需检查真实验证器产生的错误集合,不能把辅助器不再抛异常当作模型已经合法。


