Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using FluentValidator to validate children of properties

I want to use FluentValidation to validate some classes one of which is only used as a property on another... but I never directly create the child class so I want to test validation from the parent level. This may be unnecessary / crazy

So for example I have

public class Parent
{
  public string Text {get;set;}
  public Child Child {get;set;}
}

public class Child 
{
  public string Text {get;set;}
}

and

public class ParentValidator : AbstractValidator<Parent>
{
  public ParentValidator() 
  {
    RuleFor(p=>p.Text).NotEmpty();
  //RuleFor(p=>p.Child).SetValidator(new ChildValidator);
  //RuleFor(p=>p.Child.Text).NotEmpty();
  }
}

public class ChildValidator : AbstractValidator<Child>
{
  public ChildValidator() 
  {
    RuleFor(c=>c.Text).NotEmpty();
  }
}

which I test using

    [Test]
    public void ParentMustHaveText()
    {
        new ParentValidator()
             .ShouldHaveValidationErrorFor(p => p.Text, "");
    }
    [Test]
    public void ChildMustHaveText()
    {
        new ParentValidator().ShouldHaveValidationErrorFor(p => p.Child.Text, "");
    }

The ChildMustHaveText test always fails no matter how I set things up. Am I being crazy trying to test it that way?

since the following test always passes

    [Test]
    public void ChildMustHaveText()
    {
        new ChildValidator().ShouldHaveValidationErrorFor(c => c.Text, "");
    }

The classes are models in an ASP.NET WebApi Project.

like image 801
Paul D'Ambra Avatar asked Jan 14 '15 14:01

Paul D'Ambra


1 Answers

The first error is that you forget to specify creation of Child property object in default Parent constructor — FluentValidation try to set dynanically property of null.

public class Parent
{
    public Parent()
    {
        Child = new Child();
    }

    public string Text { get; set; }
    public Child Child { get; set; }
}

Notice that default constructor always uses in ShouldHaveValidationErrorFor for object creation before validation.

The next thing I found is that current implementation of ShouldHaveValidationErrorFor doesn't allow to check validity of nested properties with nesting level more than 1 (obj.Child1.Child2.Text is level 3 of nesting, for example).

PITFALL

Source code of buggy place (FluentValidation.TestHelper.ValidatorTester class):

public void ValidateError(T instanceToValidate) {
        accessor.Set(instanceToValidate, value);
        // 
        var count = validator.Validate(instanceToValidate, ruleSet: ruleSet).Errors.Count(x => x.PropertyName == accessor.Member.Name);

        if (count == 0) {
            throw new ValidationTestException(string.Format("Expected a validation error for property {0}", accessor.Member.Name));
        }
    }

EXPLANATION

Method compares joined property names with validation errors (x.PropertyName) with property object System.Reflection.RuntimePropertyInfo name (accessor.Member.Name), e.g. "Text" and "Child.Text" with "Text" for both tests, so test pass only because of parent.Text is null, it's not valid and property names equal to each other in both classes.

If simplify — now your test passes, but by wrong reason.

You can see this strange behavior if you rename one of string property:

public class Child 
{
    public string Text2 {get;set;}
}

or if you make Parent.Text property valid in tests (remove rule, or initialize in Parent() default constructor by not empty value).

public Parent()
{
    Child = new Child();
    Text = "I like pitfalls";
}

CONCLUSION

It's a bug in TestHelper class, and I hope this research helps you to decide on future test strategy for your application.

And never give up! ;-)

like image 198
Evgeny Levin Avatar answered Oct 01 '22 13:10

Evgeny Levin