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
我在 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