通过 MediatR PipelineBehavior 进行单元测试验证
Unit testing validation through MediatR PipelineBehavior
我正在使用 FluentValidation 和 MediatR PipelineBehavior 来验证 CQRS 请求。我应该如何在我的单元测试中测试这种行为?
使用 FluentValidation 的 test extensions,我只测试规则。
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Should_have_error_when_name_is_empty(string recipeName)
{
validator.ShouldHaveValidationErrorFor(recipe => recipe.Name, recipeName);
}
在单元测试中手动验证请求
[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task Should_not_create_recipe_when_name_is_empty(string recipeName)
{
var createRecipeCommand = new CreateRecipeCommand
{
Name = recipeName,
};
var validator = new CreateRecipeCommandValidator();
var validationResult = validator.Validate(createRecipeCommand);
validationResult.Errors.Should().BeEmpty();
}
初始化 PipelineBehavior
[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task Should_not_create_recipe_when_name_is_empty(string recipeName)
{
var createRecipeCommand = new CreateRecipeCommand
{
Name = recipeName
};
var createRecipeCommandHandler = new CreateRecipeCommand.Handler(_context);
var validationBehavior = new ValidationBehavior<CreateRecipeCommand, MediatR.Unit>(new List<CreateRecipeCommandValidator>()
{
new CreateRecipeCommandValidator()
});
await Assert.ThrowsAsync<Application.Common.Exceptions.ValidationException>(() =>
validationBehavior.Handle(createRecipeCommand, CancellationToken.None, () =>
{
return createRecipeCommandHandler.Handle(createRecipeCommand, CancellationToken.None);
})
);
}
或者我应该使用更多这些?
ValidationBehavior class:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
throw new ValidationException(failures);
}
return next();
}
}
我觉得你所有的例子都很好。如果他们涵盖了您的代码,那么他们就提供了您所需要的。
我要描述的是一种略有不同的方法。我会提供一些背景资料。
我们在 Core (2.1) 中使用了 Mediatr、FluentValidation。我们已经包装了 Mediatr 实现,下面是我们所做的:
我们有一个通用的 pre-handler(每个处理程序只有 运行s)并为进入的 command/query 寻找 FluentValdator。如果它找不到匹配的,它只是通过。如果是,它将 运行 它,如果验证失败,将获取结果,并 return 一个 BadRequest,响应中包含我们的标准验证错误。我们还可以在业务处理程序中获取验证工厂,以便它们可以手动 运行。对开发者来说意味着更多的工作!
因此,为了测试这一点,我们使用 Microsoft.AspNetCore.TestHost 创建一个我们的测试可以命中的端点。这样做的好处是可以测试整个 Mediatr 管道(包括验证)。
所以我们有这样的事情:
var builder = WebHost.CreateDefaultBuilder()
.UseStartup<TStartup>()
.UseEnvironment(EnvironmentName.Development)
.ConfigureTestServices(
services =>
{
services.AddTransient((a) => this.SomeMockService.Object);
});
this.Server = new TestServer(builder);
this.Services = this.Server.Host.Services;
this.Client = this.Server.CreateClient();
this.Client.BaseAddress = new Uri("http://localhost");
这定义了我们的测试服务器将模拟的东西(可能是下游 http class 等)和其他各种东西。
然后我们可以访问我们实际的控制器端点。所以我们测试我们已经注册了所有内容和整个流水线。
看起来像这样(一个例子只是为了验证一下):
public SomeControllerTests(TestServerFixture testServerFixture)
:基地(测试服务器夹具)
{
}
[Fact]
public async Task SomeController_Titles_Fails_With_Expected_Validation_Error()
{
// Setup whatever you need to do to make it fail....
var response = await this.GetAsync("/somedata/titles");
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var responseAsString = await response.Content.ReadAsStringAsync();
var actualResponse = Newtonsoft.Json.JsonConvert.DeserializeObject<ValidationStuff);
actualResponse.Should().NotBeNull();
actualResponse.Should().HaveCount(1);
actualResponse.[0].Message.Should().Be("A message");
}
正如我所说,我认为您的任何选择都会满足您的需求。如果我不得不选择我的单元测试(这只是个人选择),我会选择 2) :-)
我们发现当您的处理程序管道非常简单时,更多 system/integration 测试路由非常有效。当它们变得更复杂时(我们有一个大约有 12 个处理程序加上您使用我们的包装器获得的大约 6 个处理程序)我们将它们与单独的处理程序测试一起使用,这些测试通常与您在 2) 或 3) 中所做的相匹配。
有关 system/integration 测试的更多信息,此 link 应该有所帮助。
https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api
我希望这对您有所帮助,或者至少给您一些启发:-)
我正在使用 FluentValidation 和 MediatR PipelineBehavior 来验证 CQRS 请求。我应该如何在我的单元测试中测试这种行为?
使用 FluentValidation 的 test extensions,我只测试规则。
[Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Should_have_error_when_name_is_empty(string recipeName) { validator.ShouldHaveValidationErrorFor(recipe => recipe.Name, recipeName); }
在单元测试中手动验证请求
[Theory] [InlineData("")] [InlineData(" ")] public async Task Should_not_create_recipe_when_name_is_empty(string recipeName) { var createRecipeCommand = new CreateRecipeCommand { Name = recipeName, }; var validator = new CreateRecipeCommandValidator(); var validationResult = validator.Validate(createRecipeCommand); validationResult.Errors.Should().BeEmpty(); }
初始化 PipelineBehavior
[Theory] [InlineData("")] [InlineData(" ")] public async Task Should_not_create_recipe_when_name_is_empty(string recipeName) { var createRecipeCommand = new CreateRecipeCommand { Name = recipeName }; var createRecipeCommandHandler = new CreateRecipeCommand.Handler(_context); var validationBehavior = new ValidationBehavior<CreateRecipeCommand, MediatR.Unit>(new List<CreateRecipeCommandValidator>() { new CreateRecipeCommandValidator() }); await Assert.ThrowsAsync<Application.Common.Exceptions.ValidationException>(() => validationBehavior.Handle(createRecipeCommand, CancellationToken.None, () => { return createRecipeCommandHandler.Handle(createRecipeCommand, CancellationToken.None); }) ); }
或者我应该使用更多这些?
ValidationBehavior class:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
throw new ValidationException(failures);
}
return next();
}
}
我觉得你所有的例子都很好。如果他们涵盖了您的代码,那么他们就提供了您所需要的。
我要描述的是一种略有不同的方法。我会提供一些背景资料。
我们在 Core (2.1) 中使用了 Mediatr、FluentValidation。我们已经包装了 Mediatr 实现,下面是我们所做的:
我们有一个通用的 pre-handler(每个处理程序只有 运行s)并为进入的 command/query 寻找 FluentValdator。如果它找不到匹配的,它只是通过。如果是,它将 运行 它,如果验证失败,将获取结果,并 return 一个 BadRequest,响应中包含我们的标准验证错误。我们还可以在业务处理程序中获取验证工厂,以便它们可以手动 运行。对开发者来说意味着更多的工作!
因此,为了测试这一点,我们使用 Microsoft.AspNetCore.TestHost 创建一个我们的测试可以命中的端点。这样做的好处是可以测试整个 Mediatr 管道(包括验证)。
所以我们有这样的事情:
var builder = WebHost.CreateDefaultBuilder()
.UseStartup<TStartup>()
.UseEnvironment(EnvironmentName.Development)
.ConfigureTestServices(
services =>
{
services.AddTransient((a) => this.SomeMockService.Object);
});
this.Server = new TestServer(builder);
this.Services = this.Server.Host.Services;
this.Client = this.Server.CreateClient();
this.Client.BaseAddress = new Uri("http://localhost");
这定义了我们的测试服务器将模拟的东西(可能是下游 http class 等)和其他各种东西。
然后我们可以访问我们实际的控制器端点。所以我们测试我们已经注册了所有内容和整个流水线。
看起来像这样(一个例子只是为了验证一下):
public SomeControllerTests(TestServerFixture testServerFixture) :基地(测试服务器夹具) { }
[Fact]
public async Task SomeController_Titles_Fails_With_Expected_Validation_Error()
{
// Setup whatever you need to do to make it fail....
var response = await this.GetAsync("/somedata/titles");
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var responseAsString = await response.Content.ReadAsStringAsync();
var actualResponse = Newtonsoft.Json.JsonConvert.DeserializeObject<ValidationStuff);
actualResponse.Should().NotBeNull();
actualResponse.Should().HaveCount(1);
actualResponse.[0].Message.Should().Be("A message");
}
正如我所说,我认为您的任何选择都会满足您的需求。如果我不得不选择我的单元测试(这只是个人选择),我会选择 2) :-)
我们发现当您的处理程序管道非常简单时,更多 system/integration 测试路由非常有效。当它们变得更复杂时(我们有一个大约有 12 个处理程序加上您使用我们的包装器获得的大约 6 个处理程序)我们将它们与单独的处理程序测试一起使用,这些测试通常与您在 2) 或 3) 中所做的相匹配。
有关 system/integration 测试的更多信息,此 link 应该有所帮助。 https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api
我希望这对您有所帮助,或者至少给您一些启发:-)