简体   繁体   English

9.1.3 FluentValidation 更新破坏验证器模拟了吗?

[英]Did 9.1.3 FluentValidation update ruin validators mocks?

I updated to 9.1.3 version of the package and now my validator mocks do not work.我更新到 package 的 9.1.3 版本,现在我的验证器模拟不起作用。

Somehow code runs no matter whether .Validate() returns true or false .无论.Validate()返回true还是false ,代码都会以某种方式运行。

Here is the code for the validator 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);

And here is the test failing:这是测试失败:

Message: 
      Position field validation error
      Expected: <FluentValidation.ValidationException>
      But was:  null

Validator.cs:验证器.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");
        }
    }

Dependency Injection:依赖注入:

services.AddTransient<IValidator<Position>, SampleValidator>();

Usage:用法:

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);
        }
    }

Validator Mocks in test:测试中的验证器模拟:

private Mock<IValidator<EditPositionRequest>> validatorMock;
...
validatorMock = new Mock<IValidator<Position>>();

UPDATE更新

Before the update, all tests were running perfectly.在更新之前,所有测试都运行良好。 Now they're ruined and I have to install the previous version.现在他们被毁了,我必须安装以前的版本。

Expanding on my comment:扩展我的评论:

Yes, 9.1 changed how throwing validation exceptions is handled.是的,9.1 改变了抛出验证异常的处理方式。

Some context:一些上下文:

Validator classes return a ValidationResult with an IsValid boolean property.验证器类返回带有IsValid boolean 属性的ValidationResult The ValidateAndThrow extension method checks this property, and throws an exception if IsValid is false. ValidateAndThrow扩展方法检查此属性,如果IsValid为 false,则引发异常。 If you mocked the validator, you could still use the "real" ValidateAndThrow extension method on your mock to throw an exception if your mock returned an invalid validation result.如果你模拟了验证器,你仍然可以在你的模拟上使用“真正的” ValidateAndThrow扩展方法来抛出一个异常,如果你的模拟返回了一个无效的验证结果。

In FluentValidation 9.1, the logic for throwing the exception was moved out of the extension method and into the validator class itself, in the RaiseValidationException .在 FluentValidation 9.1 中,引发异常的逻辑已从扩展方法中移出,并在RaiseValidationException中移至验证器 class 本身。 This was done so that the logic for throwing the exception could be customized (by overriding this method) which couldn't be done before when it was an extension method.这样做是为了可以自定义引发异常的逻辑(通过覆盖此方法),这在以前是扩展方法时无法做到。

// 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();
  });
}

For runtime use, this doesn't make a difference - the exception is still thrown (unless you've overridden the method to prevent this).对于运行时使用,这并没有什么区别——异常仍然被抛出(除非你重写了防止这种情况的方法)。

However, this had the side effect that if you were relying on the exception being thrown by the extension method rather than the validator, this will have an undesirable result.但是,这有副作用,如果您依赖扩展方法而不是验证器抛出的异常,这将产生不良结果。 This is really only the case when mocking the validator.只有当 mocking 验证器时才会出现这种情况。 Now when you create a mock, the exception won't be thrown because the mock doesn't behave as a real validator.现在,当您创建模拟时,将不会引发异常,因为模拟不表现为真正的验证器。

My recommendation with FluentValidation has always been "don't mock validators", instead treat them as black boxes and supply real validator instances with valid/invalid input for testing purposes - this leads to much less brittle tests in the long run.我对 FluentValidation 的建议一直是“不要模拟验证器”,而是将它们视为黑盒,并为测试目的提供具有有效/无效输入的真实验证器实例 - 从长远来看,这会导致测试的脆弱性大大降低。 However, I'm also aware that it may not be possible to rewrite your tests in this way if you already have lots of them.但是,我也知道,如果您已经有很多测试,则可能无法以这种方式重写您的测试。

As a workaround, you can mock the overload of Validate that takes a ValidationContext and check the context for the ThrowOnFailures property, and have your mock throw the exception if this is set to true.作为一种解决方法,您可以模拟采用ValidationContextValidate的重载并检查ThrowOnFailures属性的上下文,如果将其设置为 true,则让您的模拟抛出异常。

However, be aware that if you do this you could run into a situation where your mock behaves one way and the real validator behaves differently (if its RaiseValidationException message has been overridden).但是,请注意,如果您这样做,您可能会遇到这样一种情况:您的模拟行为方式是一种方式,而真正的验证器行为方式不同(如果它的 RaiseValidationException 消息已被覆盖)。

As this is a breaking change, shouldn't it have been made in a major version?由于这是一个重大更改,不应该在主要版本中进行吗? Ideally yes, this was my bad as I didn't foresee this particular use case.理想情况下是的,这是我的错,因为我没有预见到这个特殊的用例。

Edit: Here's an example of creating a mock that checks the ThrowOnFailures property.编辑:这是一个创建检查ThrowOnFailures属性的模拟的示例。 The example uses the Moq library, but the same concept will apply to other mocking libraries too.该示例使用 Moq 库,但同样的概念也适用于其他 mocking 库。

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;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM