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

标签 c# unit-testing cqrs fluentvalidation mediatr

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

  • 使用 test extensions FluentValidation,我只测试规则。
    [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();
    }
    
  • 初始化管道行为
    [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);
            })
        );
    }
    

  • 还是我应该使用更多这些?

    验证行为类:
    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 实现,这就是我们所做的:

    我们有一个通用的预处理程序(只为每个处理程序运行)并为进来的命令/查询寻找一个 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");
    

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

    然后我们可以点击我们实际的 Controller 端点。所以我们测试我们已经注册了所有东西和整个管道。

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

    公共(public) SomeControllerTests(TestServerFixture 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) :-)

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

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

    我希望这会有所帮助,或者至少能给你一些思考:-)

    关于c# - 通过 MediatR PipelineBehavior 进行单元测试验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60687927/

    相关文章:

    domain-driven-design - CQRS-命令是否应尝试创建 “complex”主从实体?

    unit-testing - 什么是针对 F# 中的 nan 值进行属性测试的简洁通用方法?

    c# - 将泛型类型传递给泛型接口(interface)

    c# - 如何在 Linux 上的 asp.net 核心中捕获退出信号?

    c# - 使用c#将c++ dll注入(inject)exe

    unit-testing - 在进程中设置 "late"测试用例的最佳方法

    html - 使用 Karma 生成 HTML 测试报告

    cqrs - 如何实例化 Mediatr 作为单元测试的一部分?

    domain-driven-design - DDD\ES\CQRS 中应用程序的 CRUD 部分

    c# - 如何将纯色设置为整个菜单的背景?