FluentValidation 测试不处理预期的空值

FluentValidation Testing Not Handling Expected Nulls

我正在使用很棒的 FluentValidation 库,并且 运行 遇到了验证器单元测试中的问题。我尽可能地遵循了这个 wiki 页面上的示例(但我使用的是 xUnit)https://github.com/JeremySkinner/FluentValidation/wiki/g.-Testing

使用 ShouldHaveValidationErrorFor 扩展方法的测试。

我收到 NullReferenceException 测试失败。但这正是我正在测试的 - 必填字段的空引用。

这是我的代码:

验证者:

public class ChangeEmailRequestInputModelValidator : AbstractValidator<ChangeEmailRequestInputModel>
{
    public ChangeEmailRequestInputModelValidator()
    {
        RuleFor(x => x.NewEmail)
            .NotEmpty(); 

        RuleFor(m => m.NewEmail.Trim())
            .EmailAddress()
            .When(m => m.NewEmail != null)
            .WithMessage(ValidationConstants.SymbolIsNotAValidEmailAddress, x => x.NewEmail)
            .WithName("NewEmail"); 

        RuleFor(m => m.NewEmailConfirm.Trim())
            .Cascade(CascadeMode.Continue)
            .NotEmpty().WithMessage("Confirm New Email field cannot be empty.")
            .Equal(m => m.NewEmail.Trim()).WithMessage("Confirm New Email field must be equal to the New Email.");
    }
}

测试:

public class ChangeEmailRequestInputModelValidatorTests
{
    [Fact]
    public void Errors_Where_NewEmail_Is_Null()
    {
        var val = new ChangeEmailRequestInputModelValidator();

        val.ShouldHaveValidationErrorFor(v => v.NewEmail, null as string);            
    }
}

知道为什么会这样吗?我一定是哪里出了问题,但是它看起来和我莫名其妙的例子很相似。

您设置了三个规则:

  1. NewEmail 不能为空
  2. 如果 NewEmail 不为空,trim 它并确保它是一个有效的电子邮件地址
  3. Trim NewEmailConfirm,确保它不为空,将它与 trimmed NewEmail 进行比较并确保它匹配。

这是导致问题的第三条规则。 FluentValidation 的工作方式意味着第一个要评估的表达式是传递给 RuleFor 的表达式,除非您使用 WhenUnless,这两个表达式都通过规则回溯并应用传递给那些条件方法的谓词或反向谓词。

因此,从本质上讲,您的验证器启动了,它通过了 3 条规则中的 2 条,然后开始评估第三条规则。您的测试夹具没有为 NewEmailConfirm 设置值,因此它开始评估表达式链并命中第一个表达式,即 m => m.NewEmailConfirm.Trim(),然后它会爆炸。

您可以采取以下措施来防止这种情况发生:

[Fact]
public void Errors_Where_NewEmail_Is_Null()
{
    var sut = new ChangeEmailRequestInputModelValidator();
    sut.ShouldHaveValidationErrorFor(v => v.NewEmail, new ChangeEmailRequestInputModel { NewEmail = null, NewEmailConfirm = "demo.email@example.com" });
}

这将使用正确的位实例化您的 class-under-validation,以免在第一个表达式上爆炸。您现在要面对的问题是,在某个时候您将达到 .Equal(m => m.NewEmail.Trim())。为了测试规则 2,您的夹具显式将其设置为 null,因此规则 3 仍需要重组。

我建议如下:

public ChangeEmailRequestInputModelValidator(){

    RuleFor(m => m.NewEmail)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty()
        .WithMessage("Email is a required field.")
        .EmailAddress()
        .WithMessage(
            ValidationConstants.SymbolIsNotAValidEmailAddress, x => x.NewEmail)
        .WithName("NewEmail");

    RuleFor(m => m.NewEmailConfirm)
        .NotEmpty()
        .WithMessage("Confirm New Email field cannot be empty.");

    RuleFor(m => m)
        .Must(HaveMatchingEmailAndConfirmEmail)
        .WithMessage("Confirm New Email field must be equal to the New Email.");
}

private bool HaveMatchingEmailAndConfirmEmail(ChangeEmailRequestInputModel model)
{
    return model.NewEmail?.Trim() == model.NewEmailConfirm?.Trim();
}

以上仍然设法验证您的两个属性,彼此独立。然后它只是相互检查两个属性,利用 null 合并运算符来绕过显式 null 检查。