Search code examples
c#nunit.net-5fluentvalidation

How to unit test a single Fluent Validation Rule wit NUnit?


I've got a fluent validator config which looks like this:

        public class Validator : AbstractValidator<Command>
        {
            public Validator(IDbContext dbContext)
            {
                RuleFor(c => c.PersonId)
                    .EntityExists<Command, Person>(dbContext, nameof(Command.PersonId));

                RuleFor(c => c.ProjectId)
                    .Cascade(CascadeMode.Stop)
                    .EntityExists<Command, Project>(dbContext, nameof(Command.ProjectId))
                    .MustAsync(async (projectId, cancellation) => !(await dbContext.FindAsync<Project>(new object[] { projectId }, cancellation)).IsCompleted)
                    .WithMessage("The project is completed. You may not set the participation of a completed project");

                RuleFor(c => c.StatusId)
                    .EntityExists<Command, Status>(dbContext, nameof(Command.StatusId))

                ...
            }
        }

I'd like to unit test the second rule with NUnit, NSubstitute and the Fluent Validation Extensions:

        [Test]
        public void Should_Not_Have_Validation_Error_If_Valid_ProjectId_Is_Supplied()
        {
            _dbContext.EntityExistsAsync<Project>(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(true);
            _validator.ShouldNotHaveValidationErrorFor(command => command.ProjectId, Guid.NewGuid());
        }

The test is failing due to the missing PersonId. I'm testing the validation rule for the ProjectId, but the test includes the validation rule for the PersonId. If I mock the dbContext to make the PersonId rule pass, the test failes because of the missing StatusId which is also a separately defined validation rule.

How can I test a rule for one separate property without having to mock the rules for all other properties in the model?


Solution

  • With the hint from @Евгений Елисеев (thanks!) I was able to write test extensions which test only the rules of a single property:

            public static IEnumerable<ValidationFailure> ShouldHaveValidationErrorForExact<T, TValue>(this IValidator<T> validator,
                Expression<Func<T, TValue>> expression, TValue value) where T : class, new()
            {
                var instanceToValidate = new T();
    
                var memberAccessor = new MemberAccessor<T, TValue>(expression, true);
                memberAccessor.Set(instanceToValidate, value);
    
                TestValidationResult<T> testValidationResult = validator.TestValidate(instanceToValidate, opt => opt.IncludeProperties(memberAccessor.Member.Name));
                return testValidationResult.ShouldHaveValidationErrorFor(expression);
            }
    
            public static IEnumerable<ValidationFailure> ShouldHaveValidationErrorForExact<T, TValue>(this IValidator<T> validator, Expression<Func<T, TValue>> expression, T objectToTest) where T : class
            {
                TValue value = expression.Compile()(objectToTest);
                var memberAccessor = new MemberAccessor<T, TValue>(expression, true);
                TestValidationResult<T> testValidationResult = validator.TestValidate(objectToTest, opt => opt.IncludeProperties(memberAccessor.Member.Name));
                return testValidationResult.ShouldHaveValidationErrorFor(expression);
            }
    
            public static void ShouldNotHaveValidationErrorForExact<T, TValue>(this IValidator<T> validator,
                Expression<Func<T, TValue>> expression, TValue value) where T : class, new()
            {
    
                var instanceToValidate = new T();
    
                var memberAccessor = new MemberAccessor<T, TValue>(expression, true);
                memberAccessor.Set(instanceToValidate, value);
    
                TestValidationResult<T> testValidationResult = validator.TestValidate(instanceToValidate, opt => opt.IncludeProperties(memberAccessor.Member.Name));
                testValidationResult.ShouldNotHaveValidationErrorFor(expression);
            }
    
            public static void ShouldNotHaveValidationErrorForExact<T, TValue>(this IValidator<T> validator, Expression<Func<T, TValue>> expression, T objectToTest) where T : class
            {
                TValue value = expression.Compile()(objectToTest);
                var memberAccessor = new MemberAccessor<T, TValue>(expression, true);
                TestValidationResult<T> testValidationResult = validator.TestValidate(objectToTest, opt => opt.IncludeProperties(memberAccessor.Member.Name));
                testValidationResult.ShouldNotHaveValidationErrorFor(expression);
            }