Search code examples
c#fluentvalidationnsubstitutefluent-assertions

NullReference exception throw by FluentValidation when mocking child validators


Starting with something simple:

public interface IChild
{
  string Value { get; }
}

public class ChildValidator : AbstractValidator<IChild>
{
  public ChildValidator()
  {
    RuleFor(c => c.Value)
      .NotEmpty()
      .NotEmpty()
      .WithMessage("Friendly Error Message");
  }
}

And then testing it:

static void Test_ChildValidator()
{
  var child = Substitute.For<IChild>();
  var validator = new ChildValidator();

  child.Value.Returns(null as string);
  validator.Validate(child).IsValid.Should().BeFalse();

  child.Value.Returns("");
  validator.Validate(child).IsValid.Should().BeFalse();

  child.Value.Returns("a");
  validator.Validate(child).IsValid.Should().BeTrue();
}

No Exceptions.

Create a parent object and validator:

public interface IParent
{
  IChild Child { get; }
}

public class ParentValidator : AbstractValidator<IParent>
{
  public ParentValidator(IValidator<IChild> childValidator)
  {
    When(p => p.Child != null, () => {
      RuleFor(p => p.Child)
        .SetValidator(childValidator);
    });
  }
}

Then test that with a real child validator:

static void Test_ParentValidator_WithRealChildValidator()
{
  var child = Substitute.For<IChild>();
  var childValidator = new ChildValidator();

  var parent = Substitute.For<IParent>();
  var validator = new ParentValidator(childValidator);

  parent.Child.Returns(null as IChild);
  validator.Validate(parent).IsValid.Should().BeTrue();

  parent.Child.Returns(child);
  validator.Validate(parent).IsValid.Should().BeFalse();

  child.Value.Returns("a");
  validator.Validate(parent).IsValid.Should().BeTrue();
}

No Exceptions.

Now I tried to Mock the Child Validator (Eventually I just want to make sure that when the Child object is null or is not null, the child validator Validate method is or isn't called).

static void Test_ParentValidator_WithMockedChildValidator()
{
  var child = Substitute.For<IChild>();
  var childValidator = Substitute.For<IValidator<IChild>>();

  var parent = Substitute.For<IParent>();
  var validator = new ParentValidator(childValidator);

  parent.Child.Returns(null as IChild);
  validator.Validate(parent).IsValid.Should().BeTrue();

  parent.Child.Returns(child);

  childValidator.Validate(Arg.Any<IChild>())
    .Returns(
      new ValidationResult(
        new List<ValidationFailure> { new ValidationFailure("property", "message") }));
  validator.Validate(parent).IsValid.Should().BeFalse();

  childValidator.Validate(Arg.Any<IChild>())
    .Returns(new ValidationResult());
  validator.Validate(parent).IsValid.Should().BeTrue();
}

Throws a NullReferenceException

Source: "FluentValidation"

StackTrace:

at FluentValidation.Validators.ChildValidatorAdaptor.Validate(PropertyValidatorContext context) in

/home/jskinner/code/FluentValidation/src/FluentValidation/Validators/ChildValidatorAdaptor.cs:line 56

at FluentValidation.Internal.PropertyRule.InvokePropertyValidator(ValidationContext context, IPropertyValidator validator, String propertyName) in

/home/jskinner/code/FluentValidation/src/FluentValidation/Internal/PropertyRule.cs:line 442

at FluentValidation.Internal.PropertyRule.d__65.MoveNext()

in /home/jskinner/code/FluentValidation/src/FluentValidation/Internal/PropertyRule.cs:line 282

at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()

at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()

at FluentValidation.AbstractValidator1.Validate(ValidationContext1 context) in

/home/jskinner/code/FluentValidation/src/FluentValidation/AbstractValidator.cs:line 115

at FluentValidation.AbstractValidator`1.Validate(T instance) in /home/jskinner/code/FluentValidation/src/FluentValidation/AbstractValidator.cs:line 83

at SubValidationTest.Program.Test_ParentValidator_WithMockedChildValidator()

Is there something else I need to mock on the mocked validator to make this work correctly?

pastebin - full source code

I was not able to get this code working (at all) on DotNetFiddle :(


Solution

  • From the stack trace it looks like it fails on

     FluentValidation.AbstractValidator1.Validate(ValidationContext1 context)
    

    which was not one of the member configured on the mock.

    This should behave as expected

    [TestMethod]
    public void Test_ParentValidator_WithMockedChildValidator() {
        var child = Substitute.For<IChild>();
        var childValidator = Substitute.For<IValidator<IChild>>();
        var parent = Substitute.For<IParent>();
        var validator = new ParentValidator(childValidator);
        parent.Child.Returns(null as IChild);
    
        validator.Validate(parent).IsValid.Should().BeTrue();
    
        parent.Child.Returns(child);
        var failedResult = new ValidationResult(new List<ValidationFailure> { new ValidationFailure("property", "message") });
        childValidator.Validate(Arg.Any<ValidationContext>()).Returns(failedResult);
    
        validator.Validate(parent).IsValid.Should().BeFalse();
    
        var validResult = new ValidationResult();
        childValidator.Validate(Arg.Any<ValidationContext>()).Returns(validResult);
    
        validator.Validate(parent).IsValid.Should().BeTrue();
    }