TryValidateModel in asp.net core throws Null Reference Exception while performing unit test
Asked Answered
D

5

22

I'm trying to write unit tests for ModelState validation for an Asp.Net Core Web API.

I read that, the best way to do so is to use TryValidateModel function. But, every time I run the unit test, it throws NullReference exception.
I found many articles suggesting controller.ModelState.AddModelError("",""), but I'm not interested in this, as I believe that it beats the actual purpose of the real model validation.

[TestMethod]
public void TestMethod1()
{
    var controller = new TestController();

    controller.Post(new Model());
}


public class TestController : Controller
{
    public IActionResult Post(Model model)
    {
        bool b = TryValidateModel(model)

        return Ok();
    }
}

TryValidateModel(model) always throws NullReference Exception from TryValidateModel(model, prefix) function.

Appreciate any help.

Dedra answered 9/8, 2018 at 18:8 Comment(7)
Shouldn't controller.TestModel(new Model()); be controller.Post(new Model());Ironsides
I don't know where you read the non-sense of TryValidateModel being best practice, but thats definitely not true. First, all the official tutorials used (or still use) ModelState.IsValid. Second, with ASP.NET Core 2.1 a new [ApiController] attribute has been added, which reduces the number of things one has to do in WebApi-esque controllers. Among them, is that models implicitly validated, so that ModelState.IsValid within the controller action isn't necessary and the validation action filter returns the appropriate "problem details" (also 2.1 feature)Ambie
Read ASP.NET Core 2.1.0-preview1: Improvements for building Web APIs to learn more details about itAmbie
I updated the code to fix the wrong function call . Thanks Marcus for the tip.Dedra
@Ambie This works fine with ModelState.IsValid when I deploy the application. The concern is when I unit test, ModelState.IsValid always returns true. It does not actually perform the model validations, which is where TryValidateModel(model) comes in, which is expected to forcefully validate. But, it is throwing Null Reference Exception for me.Dedra
Please read what i wrote :P validation happens during binding and this doesn't happen when you just pass the model. Use integration tests, for realistic model validation. unit tests are not suitable for thatAmbie
Also keep in mind, if you ever switch to [ApiController], all of your logic within the controller and your unit tests based on it become obsolete, since in real word the action will never be called if there is a model validation error when using attributed based model validation (either by creating your own action attribute that checks the validation or by using ApiControllerAttribute)Ambie
S
19

It's configuration/integration issue.

You can see some additional info in the issue in ASP.NET Core repo and another one on github. But I can tell you the easiest fix (I used it once)

        var objectValidator = new Mock<IObjectModelValidator>();
        objectValidator.Setup(o => o.Validate(It.IsAny<ActionContext>(), 
                                          It.IsAny<ValidationStateDictionary>(), 
                                          It.IsAny<string>(), 
                                          It.IsAny<Object>()));
        controller.ObjectValidator = objectValidator.Object;
Sharitasharity answered 9/8, 2018 at 19:12 Comment(3)
I'm not sure that's what the OP is asking for, as it effectively always passes. If I get the op right he wants to test the validation itselfAmbie
Yes, this is basically mocking the validation which will always be true. The OP wasn't asking for this, see his answer below.Downstream
This is the correct answer IMO. If you want to test the validation, then you should probably test that seperately with the model itself. I used this to test something related to validation but in the end I could structure the test to not explicitly use validation.Kampong
D
4

As I figured how to fix Null Reference Exception thanks to @Egorikas, I noticed that it still doesn't actually validate the model and always returns a true.

I found that we could just use Validator class in System.ComponentModel.DataAnnotationsnamespace.

[TestMethod]
public void TestMethod1()
{
    var model = new Person();
    var validationResultList = new List<ValidationResult>();


    bool b1 = Validator.TryValidateObject(model, new ValidationContext(model), validationResultList);
}

You can directly validate it from the Test method itself, rather than having to call the controller, if ModelState validation is your intention.

Hope this helps.

Dedra answered 10/8, 2018 at 5:40 Comment(5)
This doesn't work for me. It will still return true :(Tweet
@PresidentCamacho I got it to work for me for anything that would cause my API to return a 400. Could you give a little more information like what sort of properties are decorated with what sort of validation attributes?Ruppert
One important note here, that the validation is not recursive, meaning only the top level is validated this way. Let's say there is a complex type Address in Person and you want to validate that this way, you need another line like: bool b2 = Validator.TryValidateObject(model.Address, new ValidationContext(model.Address), validationResultList);Downstream
@Downstream yes I noticed that :)Tweet
To have it validate all properties and attributes. Add a true as last parameter to TryValidateObjet. Like so: Validator.TryValidateObject(model, new ValidationContext(model), validationResultList, true);Fabiolafabiolas
L
4

Based on Andrew Van Den Brink answer's, but with actually having the validation errors set in the ModelState.

private class ObjectValidator : IObjectModelValidator
{

    public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model)
    {
        var context = new ValidationContext(model, serviceProvider: null, items: null);
        var results = new List<ValidationResult>();

        bool isValid = Validator.TryValidateObject(
            model, context, results,
            validateAllProperties: true
        );

        if (!isValid)
            results.ForEach((r) =>
            {
                // Add validation errors to the ModelState
                actionContext.ModelState.AddModelError("", r.ErrorMessage);
            });
    }
}

Then, simply set the ObjectValidator in your controller:

controller.ObjectValidator = new ObjectValidator();
Locale answered 9/10, 2020 at 18:15 Comment(1)
Works for me. Model that the controller tries to validate is actually validated using the validation logic in the app.Distributive
S
0
public class ObjectValidator : IObjectModelValidator
    {
        private object parameter;
        public ObjectValidator(object parameter)
        {
            this.parameter = parameter;
        }
        public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model)
        {                
            var validationContext = new ValidationContext(parameter, null, null);
            System.ComponentModel.DataAnnotations.Validator.ValidateObject(model, validationContext);                
        }
    }

var model=new {}; // Model to test    
Controller controller = new Controller(); //Controller to test
controller.ObjectValidator = new ObjectValidator(model);

This one will throw exception

Sardius answered 1/10, 2020 at 9:32 Comment(0)
K
0

I have edited the answer by Egorikas to throw an error:

The callback is inspired by this answers use of Callback.

// Code mocking your controller

        var objectValidator = new Mock<IObjectModelValidator>();
        objectValidator.Setup(o => o.Validate(It.IsAny<ActionContext>(),
                                          It.IsAny<ValidationStateDictionary>(),
                                          It.IsAny<string>(),
                                          It.IsAny<object>())).Callback<ActionContext, ValidationStateDictionary, string, object>((actionContext, _, _, _) => actionContext.ModelState.AddModelError("error", "test error"));
        controller.ObjectValidator = objectValidator.Object;

The code adds an error to the validation state of the controller, which in turn means 'TryValidateModel' returns false.

Kampong answered 2/5 at 12:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.