9.1.3 FluentValidation 更新是否破坏了验证器模拟?
Did 9.1.3 FluentValidation update ruin validators mocks?
我更新到 9.1.3 版本的包,现在我的验证器模拟不起作用。
无论 .Validate()
returns true
还是 false
.
,代码都会以某种方式运行
这里是验证器 mock 的代码:
validatorMock
.Setup(x => x.Validate(It.IsAny<IValidationContext>()).IsValid)
.Returns(false);
Assert.Throws<ValidationException>(() => command.Execute(request), "Position field validation error");
repositoryMock.Verify(repository => repository.EditPosition(It.IsAny<DbPosition>()), Times.Never);
这里是失败的测试:
Message:
Position field validation error
Expected: <FluentValidation.ValidationException>
But was: null
Validator.cs:
public class SampleValidator : AbstractValidator<Position>
{
public SampleValidator()
{
RuleFor(position => position.Name)
.NotEmpty()
.MaximumLength(80)
.WithMessage("Position name is too long");
RuleFor(position => position.Description)
.NotEmpty()
.MaximumLength(350)
.WithMessage("Position description is too long");
}
}
依赖注入:
services.AddTransient<IValidator<Position>, SampleValidator>();
用法:
public class SampleCommand : ISampleCommand
{
private readonly IValidator<Position> validator;
private readonly ISampleRepository repository;
private readonly IMapper<Position, DbPosition> mapper;
public SampleCommand(
[FromServices] IValidator<Position> validator,
[FromServices] ISampleRepository repository,
[FromServices] IMapper<Position, DbPosition> mapper)
{
this.validator = validator;
this.repository = repository;
this.mapper = mapper;
}
public bool Execute(Position request)
{
validator.ValidateAndThrow(request);
var position = mapper.Map(request);
return repository.EditPosition(position);
}
}
测试中的验证器模拟:
private Mock<IValidator<EditPositionRequest>> validatorMock;
...
validatorMock = new Mock<IValidator<Position>>();
更新
更新前,所有测试都运行完美。现在他们都毁了,我必须安装以前的版本。
扩展我的评论:
是的,9.1 更改了抛出验证异常的处理方式。
一些上下文:
验证器 classes return 一个 ValidationResult
和一个 IsValid
布尔值 属性。 ValidateAndThrow
扩展方法检查此 属性,如果 IsValid
为假则抛出异常。如果你模拟了验证器,你仍然可以在你的模拟上使用“真实的”ValidateAndThrow
扩展方法来抛出异常,如果你的模拟 returned 了一个无效的验证结果。
在 FluentValidation 9.1 中,抛出异常的逻辑已从扩展方法中移出并移至验证器 class 本身,在 RaiseValidationException
中。这样做是为了可以自定义抛出异常的逻辑(通过覆盖此方法),这在它是扩展方法之前是无法完成的。
// This is the ValidateAndThrow method definition versions older than 9.1
public static void ValidateAndThrow<T>(this IValidator<T> validator, T instance) {
var result = validator.Validate(instance);
if (!result.IsValid) {
throw new ValidationException(result.Errors);
}
}
// This is the ValidateAndThrowMethod in 9.1 and newer
public static void ValidateAndThrow<T>(this IValidator<T> validator, T instance) {
validator.Validate(instance, options => {
options.ThrowOnFailures();
});
}
对于 运行time 使用,这没有什么区别 - 异常仍然会被抛出(除非你重写了防止这种情况发生的方法)。
但是,这有副作用,如果您依赖扩展方法而不是验证器抛出的异常,这将产生不良结果。只有在模拟验证器时才会出现这种情况。现在,当您创建模拟时,不会抛出异常,因为模拟不作为真正的验证器。
我对 FluentValidation 的建议一直是“不要模拟验证器”,而是将它们视为黑盒并提供带有 valid/invalid 输入的真实验证器实例以用于测试目的 - 这会大大减少脆弱性测试长运行。但是,我也知道如果您已经有很多测试,则可能无法以这种方式重写测试。
作为一种解决方法,您可以模拟 Validate
的重载,它接受 ValidationContext
并检查 ThrowOnFailures
属性 的上下文,然后让您的模拟抛出如果设置为 true,则异常。
但是,请注意,如果您这样做,您可能 运行 会遇到这样一种情况:您的模拟行为是一种方式,而真正的验证器行为是不同的(如果其 RaiseValidationException 消息已被覆盖)。
因为这是一个破坏性的变化,它不应该在一个主要版本中进行吗?理想情况下是的,这是我的错误,因为我没有预见到这个特定的用例。
编辑:下面是创建一个检查 ThrowOnFailures
属性 的模拟的示例。该示例使用 Moq 库,但同样的概念也适用于其他模拟库。
private static Mock<IValidator<T>> CreateFailingMockValidator<T>() {
var mockValidator = new Mock<IValidator<T>>();
var failureResult = new ValidationResult(new List<ValidationFailure>() {
new ValidationFailure("Foo", "Bar")
});
// Setup the Validate/ValidateAsync overloads that take an instance.
// These will never throw exceptions.
mockValidator.Setup(p => p.Validate(It.IsAny<T>()))
.Returns(failureResult).Verifiable();
mockValidator.Setup(p => p.ValidateAsync(It.IsAny<T>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(failureResult);
// Setup the Validate/ValidateAsync overloads that take a context.
// This is the method called by ValidateAndThrow, so will potentially support throwing the exception.
// Setup method invocations for with an exception and without.
mockValidator.Setup(p => p.Validate(It.Is<ValidationContext<T>>(context => context.ThrowOnFailures)))
.Throws(new ValidationException(failureResult.Errors));
mockValidator.Setup(p => p.ValidateAsync(It.Is<ValidationContext<T>>(context => context.ThrowOnFailures), It.IsAny<CancellationToken>()))
.Throws(new ValidationException(failureResult.Errors));
// If ThrowOnFailures is false, return the result.
mockValidator.Setup(p => p.Validate(It.Is<ValidationContext<T>>(context => !context.ThrowOnFailures)))
.Returns(failureResult).Verifiable();
mockValidator.Setup(p => p.ValidateAsync(It.Is<ValidationContext<T>>(context => !context.ThrowOnFailures), It.IsAny<CancellationToken>()))
.ReturnsAsync(failureResult);
return mockValidator;
}
我更新到 9.1.3 版本的包,现在我的验证器模拟不起作用。
无论 .Validate()
returns true
还是 false
.
这里是验证器 mock 的代码:
validatorMock
.Setup(x => x.Validate(It.IsAny<IValidationContext>()).IsValid)
.Returns(false);
Assert.Throws<ValidationException>(() => command.Execute(request), "Position field validation error");
repositoryMock.Verify(repository => repository.EditPosition(It.IsAny<DbPosition>()), Times.Never);
这里是失败的测试:
Message:
Position field validation error
Expected: <FluentValidation.ValidationException>
But was: null
Validator.cs:
public class SampleValidator : AbstractValidator<Position>
{
public SampleValidator()
{
RuleFor(position => position.Name)
.NotEmpty()
.MaximumLength(80)
.WithMessage("Position name is too long");
RuleFor(position => position.Description)
.NotEmpty()
.MaximumLength(350)
.WithMessage("Position description is too long");
}
}
依赖注入:
services.AddTransient<IValidator<Position>, SampleValidator>();
用法:
public class SampleCommand : ISampleCommand
{
private readonly IValidator<Position> validator;
private readonly ISampleRepository repository;
private readonly IMapper<Position, DbPosition> mapper;
public SampleCommand(
[FromServices] IValidator<Position> validator,
[FromServices] ISampleRepository repository,
[FromServices] IMapper<Position, DbPosition> mapper)
{
this.validator = validator;
this.repository = repository;
this.mapper = mapper;
}
public bool Execute(Position request)
{
validator.ValidateAndThrow(request);
var position = mapper.Map(request);
return repository.EditPosition(position);
}
}
测试中的验证器模拟:
private Mock<IValidator<EditPositionRequest>> validatorMock;
...
validatorMock = new Mock<IValidator<Position>>();
更新
更新前,所有测试都运行完美。现在他们都毁了,我必须安装以前的版本。
扩展我的评论:
是的,9.1 更改了抛出验证异常的处理方式。
一些上下文:
验证器 classes return 一个 ValidationResult
和一个 IsValid
布尔值 属性。 ValidateAndThrow
扩展方法检查此 属性,如果 IsValid
为假则抛出异常。如果你模拟了验证器,你仍然可以在你的模拟上使用“真实的”ValidateAndThrow
扩展方法来抛出异常,如果你的模拟 returned 了一个无效的验证结果。
在 FluentValidation 9.1 中,抛出异常的逻辑已从扩展方法中移出并移至验证器 class 本身,在 RaiseValidationException
中。这样做是为了可以自定义抛出异常的逻辑(通过覆盖此方法),这在它是扩展方法之前是无法完成的。
// This is the ValidateAndThrow method definition versions older than 9.1
public static void ValidateAndThrow<T>(this IValidator<T> validator, T instance) {
var result = validator.Validate(instance);
if (!result.IsValid) {
throw new ValidationException(result.Errors);
}
}
// This is the ValidateAndThrowMethod in 9.1 and newer
public static void ValidateAndThrow<T>(this IValidator<T> validator, T instance) {
validator.Validate(instance, options => {
options.ThrowOnFailures();
});
}
对于 运行time 使用,这没有什么区别 - 异常仍然会被抛出(除非你重写了防止这种情况发生的方法)。
但是,这有副作用,如果您依赖扩展方法而不是验证器抛出的异常,这将产生不良结果。只有在模拟验证器时才会出现这种情况。现在,当您创建模拟时,不会抛出异常,因为模拟不作为真正的验证器。
我对 FluentValidation 的建议一直是“不要模拟验证器”,而是将它们视为黑盒并提供带有 valid/invalid 输入的真实验证器实例以用于测试目的 - 这会大大减少脆弱性测试长运行。但是,我也知道如果您已经有很多测试,则可能无法以这种方式重写测试。
作为一种解决方法,您可以模拟 Validate
的重载,它接受 ValidationContext
并检查 ThrowOnFailures
属性 的上下文,然后让您的模拟抛出如果设置为 true,则异常。
但是,请注意,如果您这样做,您可能 运行 会遇到这样一种情况:您的模拟行为是一种方式,而真正的验证器行为是不同的(如果其 RaiseValidationException 消息已被覆盖)。
因为这是一个破坏性的变化,它不应该在一个主要版本中进行吗?理想情况下是的,这是我的错误,因为我没有预见到这个特定的用例。
编辑:下面是创建一个检查 ThrowOnFailures
属性 的模拟的示例。该示例使用 Moq 库,但同样的概念也适用于其他模拟库。
private static Mock<IValidator<T>> CreateFailingMockValidator<T>() {
var mockValidator = new Mock<IValidator<T>>();
var failureResult = new ValidationResult(new List<ValidationFailure>() {
new ValidationFailure("Foo", "Bar")
});
// Setup the Validate/ValidateAsync overloads that take an instance.
// These will never throw exceptions.
mockValidator.Setup(p => p.Validate(It.IsAny<T>()))
.Returns(failureResult).Verifiable();
mockValidator.Setup(p => p.ValidateAsync(It.IsAny<T>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(failureResult);
// Setup the Validate/ValidateAsync overloads that take a context.
// This is the method called by ValidateAndThrow, so will potentially support throwing the exception.
// Setup method invocations for with an exception and without.
mockValidator.Setup(p => p.Validate(It.Is<ValidationContext<T>>(context => context.ThrowOnFailures)))
.Throws(new ValidationException(failureResult.Errors));
mockValidator.Setup(p => p.ValidateAsync(It.Is<ValidationContext<T>>(context => context.ThrowOnFailures), It.IsAny<CancellationToken>()))
.Throws(new ValidationException(failureResult.Errors));
// If ThrowOnFailures is false, return the result.
mockValidator.Setup(p => p.Validate(It.Is<ValidationContext<T>>(context => !context.ThrowOnFailures)))
.Returns(failureResult).Verifiable();
mockValidator.Setup(p => p.ValidateAsync(It.Is<ValidationContext<T>>(context => !context.ThrowOnFailures), It.IsAny<CancellationToken>()))
.ReturnsAsync(failureResult);
return mockValidator;
}