Fluent Validation:使用自定义属性验证器的单元测试未出预期错误怎么办?
你的问题出在验证器初始化时机和依赖传递方式上,以下是具体原因和修复步骤:
核心问题
你在定义验证规则时,直接在SetValidator中new UniqueValueValidator并传入了_repository.Queryable()——这会在验证器构造阶段就获取到IQueryable实例,而不是在验证请求发生时动态获取。后续单元测试中mock的_peopleRepositoryMock.Setup(x => x.Queryable()).Returns(people)并不会生效,因为验证器内部已经持有了构造时的旧IQueryable对象,根本不会使用你mock后的数据源。
修复方案
不要直接给验证器传入IQueryable,而是传入整个Repository实例,让验证器在验证逻辑执行时动态调用Queryable()获取数据源。
第一步:修改UniqueValueValidator的构造逻辑
调整验证器,让它依赖Repository而非直接依赖IQueryable:
public class UniqueValueValidator<TInput, TEntity, TProperty> : AbstractValidator<TInput> { private readonly IRepository<TEntity> _repository; private readonly Expression<Func<TEntity, TProperty>> _entityPropertySelector; private readonly Expression<Func<TInput, TProperty>> _inputPropertySelector; // 构造函数接收Repository和属性选择器,而非直接传入IQueryable public UniqueValueValidator( IRepository<TEntity> repository, Expression<Func<TEntity, TProperty>> entityPropertySelector, Expression<Func<TInput, TProperty>> inputPropertySelector) { _repository = repository; _entityPropertySelector = entityPropertySelector; _inputPropertySelector = inputPropertySelector; // 验证逻辑:动态查询Repository的Queryable RuleFor(_inputPropertySelector) .MustAsync(async (inputModel, propertyValue, cancellationToken) => { // 验证时才获取最新的IQueryable var exists = await _repository.Queryable() .AnyAsync(entity => EqualityComparer<TProperty>.Default.Equals( _entityPropertySelector.Compile()(entity), propertyValue ), cancellationToken); return !exists; }) .WithMessage("Duplicate value"); } }
第二步:调整验证规则的定义
在你的PersonValidator中,修改为传入Repository和属性选择器:
RuleFor(e => e.Name) .SetValidator(new UniqueValueValidator<PersonInput, Person, string>( _repository, entity => entity.Name, input => input.Name )) .WithMessage("Duplicate name");
第三步:修正单元测试逻辑
另外注意你的测试方法名ShouldNotHaveValidationError_WhenNameIsUnique和测试逻辑矛盾——你传入的是重复的Name,应该断言存在验证错误,同时确保mock的数据源正确包含重复值:
public void ShouldHaveValidationError_WhenNameIsDuplicate() { // 构造包含"Al"的模拟数据源 var people = new List<Person> { new Person { Name = "Al" } }.AsQueryable(); _peopleRepositoryMock.Setup(x => x.Queryable()).Returns(people); // 传入重复的Name var model = new PersonInputModel{ Name = "Al" }; var result = _validator.TestValidate(model); // 断言Name字段存在验证错误 result.ShouldHaveValidationErrorFor(person => person.Name); }
关于是否需要模拟UniqueValueValidator
不需要。UniqueValueValidator是你自定义的验证逻辑,属于需要测试的目标对象之一,模拟它会让测试失去意义。你只需要模拟它依赖的外部资源(比如Repository),以此控制测试数据,验证它的逻辑是否正确。
内容的提问来源于stack exchange,提问作者Temp034

