通过 MediatR PipelineBehavior 进行单元测试验证

Val*_*ász 6 c# unit-testing cqrs fluentvalidation mediatr

我正在使用 FluentValidation 和 MediatR PipelineBehavior 来验证 CQRS 请求。我应该如何在单元测试中测试这种行为?

  1. 使用FluentValidation 的测试扩展,我仅测试规则。

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Should_have_error_when_name_is_empty(string recipeName)
    {
        validator.ShouldHaveValidationErrorFor(recipe => recipe.Name, recipeName);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 在单元测试中手动验证请求

    [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();
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 初始化Pipeline行为

    [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);
            })
        );
    }
    
    Run Code Online (Sandbox Code Playgroud)

或者我应该使用更多这些?

ValidationBehavior 类:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

小智 4

我认为你所有的例子都很好。如果他们涵盖了您的代码,那么他们就提供了您所需要的内容。

我要描述的是一种略有不同的方法。我将提供一些背景知识。

我们在 Core (2.1) 中使用 Mediatr、FluentValidation。我们已经包装了 Mediatr 实现,这就是我们所做的:

我们有一个通用的预处理程序(只为每个处理程序运行),并为传入的命令/查询寻找 FluentValdator。如果它找不到匹配的,它就会继续。如果确实如此,它将运行它,如果失败,验证将获取结果并返回一个 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");
Run Code Online (Sandbox Code Playgroud)

这定义了我们的测试服务器将模拟的东西(可能是下游http类等)和各种其他东西。

然后我们可以访问实际的控制器端点。所以我们测试我们已经注册了所有内容和整个管道。

看起来像这样(一个例子只是为了测试一些验证):

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");
Run Code Online (Sandbox Code Playgroud)

正如我所说,我认为您的任何选择都会满足您的需要。如果我必须选择单元测试(这只是个人选择),我会选择 2) :-)

我们发现,当您的处理程序管道非常简单时,更多系统/集成测试路线非常有效。当它们变得更加复杂时(我们有大约 12 个处理程序加上大约 6 个处理程序,您只需使用我们的包装器即可获得),我们将它们与单独的处理程序测试一起使用,这些测试通常与您在 2) 或 3) 中所做的操作相匹配。

有关系统/集成测试的更多信息,此链接应该有所帮助。 https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api

我希望这对您有所帮助,或者至少给您一些思考的空间:-)