Search code examples
c#unit-testingcqrsfluentvalidationmediatr

Unit testing validation through MediatR PipelineBehavior


I'm using FluentValidation and MediatR PipelineBehavior to validate the CQRS requests. How should I test this behavior in my unit tests?

  1. Use the test extensions of FluentValidation and I test only the rules.

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Should_have_error_when_name_is_empty(string recipeName)
    {
        validator.ShouldHaveValidationErrorFor(recipe => recipe.Name, recipeName);
    }
    
  2. Manually validate the request in the unit test

    [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();
    }
    
  3. Initialize the 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);
            })
        );
    }
    

Or should I use more of these?

The 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();
    }
}

Solution

  • I think all your examples are fine. If they cover your code then they are providing what you need.

    What I going to describe is a slightly different approach. I will provide some background.

    We use Mediatr, FluentValidation in Core (2.1). We have wrapped the Mediatr implementation and here is what we do:

    We have a generic pre-handler (just runs for every handler) and looks for a FluentValdator for the command/query coming in. If it can't find one that matches, it just passes on. If it does it will run it and if it fails validation will grab the results and return a BadRequest with our standard validation guff in the response. We also have the ability to grab the a validation factory in the business handlers so they van be run manually. Just means a bit more work for the developer!

    So to test this we use Microsoft.AspNetCore.TestHost to create an endpoint our tests can hit. The benefit of this is that test the whole Mediatr pipeline (including validation).

    So we have this sort of thing:

    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");
    

    This defines things our test server will mock (possibly downstream http class etc) and various other stuff.

    We can then hit our actual controller endpoint. So we test we have registered everything and the whole pipleline.

    Looks like this (an example just to test a bit of validation):

    public SomeControllerTests(TestServerFixture<Startup> testServerFixture)
        : base(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");
        }
    

    As I say, I think any of your choices will do what you need. If I had to pick with my unit test head on (and this is just a personal choice), I would go with 2) :-)

    We have found the more system/integration test route works really well when your handler pipleline is quite simple. When they become more complex (we have one with approx 12 handlers plus around 6 that you just get by using our wrapper) we use them along with individual handler tests that usually match what you have done with either 2) or in 3).

    For more info about the system/integration tests this link should help. https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api

    I hope this helps or at least gives you some food for thought :-)