FluentValidation:模拟 ValidationResult.IsValid

FluentValidation: Mocking ValidationResult.IsValid

我在 DotNet 核心 2 中将 FluentValidation 与 WebAPI 结合使用。我已经成功地为验证器编写了测试,但现在我正在尝试为我的控制器模拟验证器。控制器如下:

[Route("[controller]")]
public class SecurityController : Controller {
    private readonly IValidator<AuthenticateRequest> _authenticateRequestValidator;

    public SecurityController(IValidator<AuthenticateRequest> authenticateRequestValidator) {
        _authenticateRequestValidator = authenticateRequestValidator;
    }

    [HttpPost]
    [AllowAnonymous]
    [Route("auth")]
    public async Task<IActionResult> AuthenticateAsync([FromBody] AuthenticateRequest req) {
        // Validate
        var validator = await _authenticateRequestValidator.ValidateAsync(req);
        if(!validator.IsValid) {
            return BadRequest();
        }

        // ...snip
    }
}

AuthenticateRequest 看起来像这样:

public class AuthenticateRequest {
    public string Username { get; set; }

    public string Password { get; set; }
}

验证器如下:

public class AuthenticateRequestValidator : AbstractValidator<AuthenticateRequest> {
    /// <summary>
    ///     Provides a validator for <see cref="AuthenticateRequest" />
    /// </summary>
    public AuthenticateRequestValidator() {
        RuleFor(x => x.Username)
            .NotNull()
            .NotEmpty()
            .WithMessage("Username is required");
        RuleFor(x => x.Password)
            .NotNull()
            .NotEmpty()
            .WithMessage("Password is required");
    }
}

正在使用 dot net core 的标准 DI 注入控制器。不发布代码,因为它与此问题无关,因为它是一个测试问题。

我正在使用 xunit、Moq 和 AutoFixture 进行测试。这里有两个测试:

public class SecurityControllerTests {
    private readonly IFixture Fixture = new Fixture().Customize(new AutoMoqCustomization {ConfigureMembers = true});

    private readonly Mock<IValidator<AuthenticateRequest>> authenticateRequestValidatorMock;

    public SecurityControllerTests() {
        authenticateRequestValidatorMock = Mock.Get(Fixture.Create<IValidator<AuthenticateRequest>>());
    }

    [Fact]
    public async Task Authenticate_ValidatesRequest() {
        // Arrange
        var request = Fixture.Create<AuthenticateRequest>();
        authenticateRequestValidatorMock
            .Setup(x => x.ValidateAsync(It.Is<AuthenticateRequest>(v => v == request), default(CancellationToken)))
            .Returns(() => Fixture.Create<Task<ValidationResult>>())
            .Verifiable();
        var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);

        // Act
        await controller.AuthenticateAsync(request);

        // Assert
        authenticateRequestValidatorMock.Verify();
    }

    [Fact]
    public async Task Authenticate_Returns400_WhenUsernameValidationFails() {
        // Arrange
        var request = Fixture.Create<AuthenticateRequest>();

        var validationResultMock = new Mock<ValidationResult>();
        validationResultMock
            .SetupGet(x => x.IsValid)
            .Returns(() => true);
        authenticateRequestValidatorMock
            .Setup(x => x.ValidateAsync(It.Is<AuthenticateRequest>(v => v == request), default(CancellationToken)))
            .Returns(() => new Task<ValidationResult>(() => validationResultMock.Object));
        var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);

        // Act
        var result = await controller.AuthenticateAsync(request);

        // Assert
        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
        Assert.IsType<SerializableError>(badRequestResult.Value);
    }
}

我需要模拟 ValidationResult 这样我就可以忽略实际的验证器逻辑(在别处测试)并测试控制器逻辑。注入了许多其他依赖项和更多代码,但粘贴的代码是问题的症结所在,当其他所有内容都被剥离时会产生相同的结果。

第一个测试通过,第二个测试在到达控制器中的 var validator = await _authenticateRequestValidator.ValidateAsync(req); 行时永远运行。

值得注意的是ValidationResult.IsValid是虚拟只读属性。

第二个测试有什么问题?

您是否尝试过 Asp.Net Core - FluentValidation 集成?通过这种方式,您不需要将 Validator depedencies 传递给构造函数。

https://github.com/JeremySkinner/FluentValidation/wiki/i.-ASP.NET-Core-integration

FluentValidation 在验证错误的情况下填充 ModelState,您可以像这样使用它;

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

为了测试它,您设置了控制器模拟的 ModelState

var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);
controller.ModelState.AddModelError("test", "test");

// Act
IActionResult actionResult =  await controller.AuthenticateAsync(request);

var badRequestObjectResult = actionResult as BadRequestObjectResult;

Assert.NotNull(badRequestObjectResult);

var serializableError = badRequestObjectResult.Value as SerializableError;

// Assert
Assert.NotNull(result);
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
var serializableError = assert.IsType<SerializableError>(badRequestResult.Value)
Assert.True(((string[])serializableError["test"])[0] == "test");

将 ModelState 留空就足以忽略我认为的实际验证器逻辑。

FluentValidation 也有内置测试 api。您可以单独测试您的验证逻辑。

https://github.com/JeremySkinner/FluentValidation/wiki/g.-Testing