ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FluentValidation 测试扩展指南:用 TestValidate 与 TestHelper 编写健壮的校验器单元测试

FluentValidation 测试扩展指南:用 TestValidate 与 TestHelper 编写健壮的校验器单元测试 后端【免费下载链接】FluentValidationA popular .NET validation library for building strongly-typed validation rules.项目地址https://gitcode.com/gh_mirrors/fl/FluentValidation点击查看免费下载导读本文围绕 FluentValidation 官方文档 docs/testing.md 展开系统讲解如何借助FluentValidation.TestHelper命名空间下的测试扩展为校验器编写单元测试。你将掌握TestValidate/TestValidateAsync的完整用法、ShouldHaveValidationErrorFor等断言链的深度技巧错误消息、错误码、严重级别、自定义状态、Only()精确匹配以及官方推荐的“黑盒测试”理念与InlineValidatorT桩实现方案。文中所有结论均对照本仓库src/FluentValidation/TestHelper/与src/FluentValidation.Tests/ValidatorTesterTester.cs的源码与测试用例可直接复制运行。测试校验器的核心思路把校验器当作“黑盒”FluentValidation 官方对测试的推荐做法非常明确将校验器视为“黑盒”——向其输入数据然后断言校验结果是否正确。也就是说测试不关心校验器内部如何组织规则是RuleFor链、依赖规则还是子校验器只关心“输入什么样的模型应该得到什么样的ValidationResult”。这种思路带来了两个直接好处测试与校验器的内部实现解耦重构规则而不破坏测试语义断言语义清晰任何人阅读测试都能立刻理解该校验器的行为契约。FluentValidation.TestHelper正是围绕这一理念设计的。它没有引入新的校验引擎而是基于IValidatorT的标准Validate/ValidateAsync接口提供了一层断言友好的包装。使用 TestValidate让断言变简单基本用法TestValidate是定义在 ValidatorTestExtensions.cs 上的扩展方法签名如下public static TestValidationResultT TestValidateT( this IValidatorT validator, T objectToTest, ActionValidationStrategyT options null)它内部就是调用validator.Validate(context)执行一次标准校验然后把ValidationResult包装成TestValidationResultT返回。TestValidationResultT继承自ValidationResult见 TestValidationResult.cs因此你仍然可以像使用普通校验结果一样访问Errors、RuleSetsExecuted等成员同时额外获得一系列Should*断言方法。假设我们有如下校验器public class PersonValidator : AbstractValidatorPerson { public PersonValidator() { RuleFor(person person.Name).NotNull(); } }使用 NUnit 编写测试using NUnit.Framework; using FluentValidation; using FluentValidation.TestHelper; [TestFixture] public class PersonValidatorTester { private PersonValidator validator; [SetUp] public void Setup() { validator new PersonValidator(); } [Test] public void Should_have_error_when_Name_is_null() { var model new Person { Name null }; var result validator.TestValidate(model); result.ShouldHaveValidationErrorFor(person person.Name); } [Test] public void Should_not_have_error_when_name_is_specified() { var model new Person { Name Jeremy }; var result validator.TestValidate(model); result.ShouldNotHaveValidationErrorFor(person person.Name); } }断言失败时抛出 ValidationTestException当断言未命中时会抛出 ValidationTestException。它继承自Exception并额外携带ErrorsListValidationFailure属性方便测试框架xUnit、NUnit、MSTest直接展示失败详情。更重要的是异常消息本身包含了诊断信息当ShouldHaveValidationErrorFor失败时消息中会列出Properties with Validation Errors及每条错误对应的属性名当ShouldNotHaveValidationErrorFor失败时消息会列出实际命中的Validation Errors。这一行为由 TestValidationResult.cs 中的ShouldHaveValidationError/ShouldNotHaveValidationError私有方法实现仓库测试 ValidatorTesterTester.cs 对此有精确断言例如Expected a validation error for property NullableInt.Value ---- Properties with Validation Errors: [0]: NullableInt对同一结果做多次断言对于复杂场景TestValidate返回的TestValidationResultT可以反复使用对单个校验结果做多次断言var person new Person { Name Jeremy }; var result validator.TestValidate(person); // 断言 Name 属性应有校验失败。 result.ShouldHaveValidationErrorFor(x x.Name); // 断言 Age 属性没有校验失败。 result.ShouldNotHaveValidationErrorFor(x x.Age); // 对难以用 lambda 表达的属性可以直接使用字符串属性名例如 result.ShouldHaveValidationErrorFor(Addresses[0].Line1);字符串形式不仅支持索引器Addresses[0].Line1也支持模型级规则。仓库测试 ValidatorTesterTester.cs 验证了ShouldHaveValidationErrorFor(Orders[0].ProductName)与反向断言ShouldNotHaveValidationErrorFor(Orders[0].ProductName)均可正常命中集合子校验器产生的失败。注意 lambda 形式与字符串形式在属性名归一化上的差异见 TestValidationResult.cslambda 形式会通过NormalizePropertyName将Addresses[0].Line1这类属性名中的[...]索引部分剔除后做比较RuleForEach场景下PropertyName形如NickNames[0]字符串形式则按原始PropertyName精确匹配。断言链深入校验失败的每个细节ShouldHaveValidationErrorFor返回ITestValidationWith接口继承自ITestValidationContinuation见 ITestValidationContinuation.cs因此可以继续链式调用以下方法逐项校验失败的组成要素var result validator.TestValidate(person); result.ShouldHaveValidationErrorFor(person person.Name) .WithErrorMessage(Name must not be empty.) .WithSeverity(Severity.Error) .WithErrorCode(NotNullValidator);完整的正向断言方法这些方法定义在 ValidatorTestExtensions.cs 中底层均基于When要求至少一条失败满足谓词方法校验维度实现位置WithErrorMessage(string)ValidationFailure.ErrorMessageL183-L185WithErrorCode(string)ValidationFailure.ErrorCodeL187-L189WithSeverity(Severity)ValidationFailure.SeverityL170-L172WithCustomState(object, IEqualityComparer null)ValidationFailure.CustomStateL174-L176WithMessageArgumentT(string key, T value)FormattedMessagePlaceholderValues中的占位参数L178-L181几个要点WithCustomState支持自定义比较器。当CustomState是通过字符串拼接等途径生成、引用不相等但值相等时如Test 123默认的Equals在引用类型上可能失败。仓库测试 ValidatorTesterTester.cs 演示了传入StringComparer.OrdinalIgnoreCase来忽略大小写比较。默认不传比较器时使用Equals(failure.CustomState, expectedCustomState)。WithMessageArgument用于校验消息占位符值。例如自定义校验器通过context.MessageFormatter.AppendArgument(Foo, bar)注入参数、消息模板写作{Foo}测试中可断言该参数实际值见 ValidatorTesterTester.cs。反向断言方法对应的逆向方法基于WhenAll要求所有失败都满足谓词即“不允许存在不满足条件的失败”result.ShouldHaveValidationErrorFor(x x.Name) .WithoutMessage(...) // 不允许出现该错误消息 .WithoutErrorCode(...) // 不允许出现该错误码 .WithoutSeverity(Severity.Warning) .WithoutCustomState(...);实现见 ValidatorTestExtensions.cs。注意Without*系列返回的是ITestValidationContinuation而非ITestValidationWith语义是“排除法”——确保结果中不存在你不想看到的失败形态。Only()精确限定失败集合如果希望确保校验失败只发生在指定条件下、没有其他意外失败可以在条件断言链末尾追加Only()var result validator.TestValidate(person); // 断言失败只发生在 Name 属性上。 result.ShouldHaveValidationErrorFor(person person.Name).Only(); // 断言失败只发生在 Name 属性上且所有失败的消息都符合指定值。 result.ShouldHaveValidationErrorFor(person person.Name) .WithErrorMessage(Name must not be empty.) .Only();Only()的实现ValidatorTestExtensions.cs会递归收集当前断言链及其父级所有“未匹配”的失败一旦发现任何未匹配项就抛出ValidationTestException并在消息中以Unexpected Errors:列表的形式逐条展示。灵活匹配ShouldHaveValidationErrors 与 ShouldNotHaveAnyValidationErrors除了按属性断言TestValidationResultT还提供不关心具体属性、只关心“有无失败”的断言// 至少存在一条校验失败返回可继续链式断言的续体。 result.ShouldHaveValidationErrors().WithErrorCode(nota); // 不允许存在任何校验失败。 result.ShouldNotHaveAnyValidationErrors();ShouldHaveValidationErrors()无任何失败时抛出异常见 TestValidationResult.cs返回的续体可继续用WithErrorCode/WithErrorMessage等筛选。仓库测试 ValidatorTesterTester.cs 演示了同一属性两条规则产生不同错误码时分别用WithErrorCode(nota)与WithErrorCode(notb)独立断言。ShouldNotHaveAnyValidationErrors()内部使用特殊标记__FV__ANY定义于 ValidatorTestExtensions.cs匹配任意失败只要存在任何失败即抛出。异步 TestValidateAsync当校验器包含异步规则如MustAsync、WhenAsync时必须使用异步版本TestValidateAsync。它的签名ValidatorTestExtensions.cs与TestValidate对应并额外支持CancellationTokenpublic static TaskTestValidationResultT TestValidateAsyncT( this IValidatorT validator, T objectToTest, ActionValidationStrategyT options null, CancellationToken cancellationToken default)用法与同步版本一致只是需要awaitvar result await validator.TestValidateAsync(model); result.ShouldHaveValidationErrorFor(x x.Name);重要陷阱如果在包含异步规则的校验器上错误地调用同步的TestValidate会抛出AsyncValidatorInvokedSynchronouslyException。TestValidate捕获该异常并重新抛出带提示信息的版本——“contains asynchronous rules - please use the asynchronous test methods instead”见 ValidatorTestExtensions.cs。仓库测试 ValidatorTesterTester.cs 对“同步调用抛异常、异步调用正常执行”两种路径均有覆盖。通过 options 定制校验策略TestValidate/TestValidateAsync的可选参数options是一个ActionValidationStrategyT委托让你在测试中精确控制“校验哪些内容”。ValidationStrategyT定义于 Internal/ValidationStrategy.cs常用方法如下方法作用IncludeProperties(params string[])/IncludeProperties(params ExpressionFuncT, object[])只校验指定属性IncludeRuleSets(params string[])只校验指定规则集IncludeRulesNotInRuleSet()校验所有不属于任何规则集的规则等价于IncludeRuleSets(default)IncludeAllRuleSets()校验所有规则等价于IncludeRuleSets(*)UseCustomSelector(IValidatorSelector)使用自定义选择器控制规则执行ThrowOnFailures()校验失败时直接抛异常而非返回结果典型场景是按规则集测试testValidator.TestValidate(new Person(), opt opt.IncludeRuleSets(Names)) .ShouldHaveValidationErrorFor(x x.Forename);仓库测试 ValidatorTesterTester.cs 验证了带规则集选择器的断言并且确认规则集外的规则如Id不会被误判为失败。从源码看这些选项最终会被组装成IValidatorSelector属性选择器、规则集选择器的组合见 ValidationStrategy.cs再构建ValidationContextT交给校验器执行。关于 Mock官方建议与 InlineValidator 桩方案为什么不建议 Mock 校验器FluentValidation 官方对 Mock 校验器持明确反对态度见 docs/testing.md 的 Mocking 小节有效校验器应作为“黑盒”使用在测试中构造已知的坏数据触发校验失败再断言结果这是最推荐的方式Mock 校验器要求你对校验器的内部构造规则组成乃至 FluentValidation 自身的内部机制做出假设导致测试脆弱且升级不友好——FluentValidation 内部任何调整都可能让 Mock 失效。必须 Mock 时的官方方案InlineValidatorT如果确实需要“伪造”一个校验器例如被测代码依赖IValidatorCustomer而真实校验器依赖外部数据库服务官方建议使用InlineValidatorT创建桩实现而不是引入 Mock 库。这样能复用 FluentValidation 自身生成校验失败的内部逻辑行为与真实校验器一致。示例原校验器依赖外部仓储检查客户 ID 是否已占用// 依赖外部服务的原始校验器 // 外部服务用于检查该客户 ID 是否已存在于数据库中。 public class CustomerValidator : AbstractValidatorCustomer { public CustomerValidator(ICustomerRepository customerRepository) { RuleFor(x x.Id) .Must(id customerRepository.CheckIdNotInUse(id)); } } // 在单元/集成测试中需要桩出该失败场景时可这样做 var validator new InlineValidatorCustomer(); validator.RuleFor(x x.Id).Must(id false); // 该实例可被传入任何期望 IValidatorCustomer 的地方。InlineValidatorT定义于 InlineValidator.cs它继承自AbstractValidatorT并提供Add方法允许通过委托追加规则。它支持两种写法// 集合初始化器写法依赖 Add 方法 var validator new InlineValidatorPerson { v v.RuleFor(x x.Surname).NotNull(), v v.RuleFor(x x.Id).NotEqual(0), }; // 直接链式写法 var validator new InlineValidatorPerson(); validator.RuleFor(x x.Surname).NotNull();由于它本质上是标准校验器前面讲到的所有TestValidate断言对它同样适用。仓库中的大量测试如 ValidatorTesterTester.cs都用InlineValidatorPerson构造被测校验器可作为参考范式。附带能力ShouldHaveChildValidator虽然文档主线聚焦于TestValidateTestHelper还提供一个按类型检查子校验器是否挂载的扩展方法源码中标注了 TODO 建议弃用因其易导致脆弱测试见 ValidatorTestExtensions.csvalidator.ShouldHaveChildValidator(x x.Address, typeof(AddressValidator));它通过validator.CreateDescriptor()获取描述信息检查指定成员上是否存在目标类型的子校验器含依赖规则DependentRules产生的子校验器支持模型级规则与集合子校验器。仓库测试 ValidatorTesterTester.cs 覆盖了命中、未命中、类型不符、集合场景、模型级场景与依赖规则场景。使用建议优先用TestValidate 行为断言替代该类结构断言仅在明确需要验证校验器组装结构时使用。实战小结与推荐实践基于文档与源码推荐按以下模式组织 FluentValidation 校验器的测试黑盒优先构造真实校验器 边界数据用TestValidate触发失败后断言一条测试一个契约每个测试聚焦一个行为如“Name 为空应报错”“Age 合法不应报错”断言链细化需要时用WithErrorMessage/WithErrorCode/WithSeverity/WithCustomState校验失败细节用Only()排除意外失败异步规则用异步断言凡出现MustAsync/WhenAsync等异步规则一律走TestValidateAsync避免同步调用抛AsyncValidatorInvokedSynchronouslyException需要隔离外部依赖时用InlineValidatorT桩避免 Mock 库带来的脆弱测试关注失败诊断断言失败抛出的ValidationTestException消息自带属性列表直接辅助定位问题无需额外调试。相关参考文件文档原文docs/testing.md测试扩展实现src/FluentValidation/TestHelper/ValidatorTestExtensions.cs断言结果类型src/FluentValidation/TestHelper/TestValidationResult.cs断言续体接口src/FluentValidation/TestHelper/ITestValidationContinuation.cs异常类型src/FluentValidation/TestHelper/ValidationTestException.cs桩校验器src/FluentValidation/InlineValidator.cs校验策略选项src/FluentValidation/Internal/ValidationStrategy.cs全面测试用例src/FluentValidation.Tests/ValidatorTesterTester.cs赞分享后端【免费下载链接】FluentValidationA popular .NET validation library for building strongly-typed validation rules.项目地址https://gitcode.com/gh_mirrors/fl/FluentValidation点击查看免费下载相关推荐如何测试FluentValidation验证器TestValidate断言扩展与单元测试最佳实践如何测试FluentValidation验证器TestValidate断言扩展与单元测试最佳实践 在 .NET 项目中 FluentValidation 是后端Awesome Go Security深度解析网络扫描与侦察工具完全指南Awesome Go Security深度解析网络扫描与侦察工具完全指南 欢迎来到Go语言安全工具的终极指南 Awesome Go Security是一Windows系统优化终极指南5个简单高效的Winhance使用技巧Windows系统优化终极指南5个简单高效的Winhance使用技巧 Winhance是一款专为Windows 10/11设计的开源系统优化工具它将复杂的系桌面应用上一篇使用 Google Workspace CLIgws从 Google Sheets 数据一键生成 Google Docs 报告下一篇Switch虚拟Amiibo系统emuiibo完整教程从零安装到满配使用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表