Posts ModelState Validation in Unit Test on Asp.Net Core
Post
Cancel

ModelState Validation in Unit Test on Asp.Net Core

Today, I fumbled upon an interesting issue.
Validating model/contract (ModelState validation) for an Asp.Net Core application using unit test.

Consider a model class as below: ​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User 
{ 
    [Required] 
    [MaxLength(10)] 
    public string FirstName { get; set; } 

    [MaxLength(10)] 
    public string MiddleName { get; set; } 

    [Required] 
    [MaxLength(10)] 
    public string LastName { get; set; } 

    public int Age { get; set; } 

    [MaxLength(20)] 
    public string Address { get; set; } 
} 

​ A sample Post method of controller would look like this : ​

1
2
3
4
5
6
7
8
9
10
[HttpPost] 
public IActionResult Post(User user) 
{ 
    if (!ModelState.IsValid) 
    { 
        return BadRequest(ModelState); 
    } 

    return Ok(); 
} 

Now while trying to write a unit test for this, it is natural that we would want to validate the ModelState or simply said, the input object.
We would expect this to work. ​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[TestMethod] 
public void TestModel() 
{ 
    var controller = new UsersController(); 
    var user = GetUser(); 

    var result = controller.Post(user); 

    Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); 
} 


private static User GetUser() 
    => new User 
    { 
        FirstName = null, 
        LastName = "Doe", 
        Age = 20, 
        Address = "Sample Address" 
    }; 

Nope. This will fail. ModelState.IsValid would return true. This is because the model binding doesn’t actually happen during a unit test.

There are a few solutions to this: ​ ​

1. Add Model Errors from the unit test itself

​ This method involves hardcoding the expected error directly into the ModelState object of the controller from the unit test itself.
​ It will look like this: ​

1
2
3
4
5
6
7
8
9
10
11
12
[TestMethod] 
public void ModelStateAddErrorTest() 
{ 
    var contoller = new UsersController(); 
    var user = GetUser(); 

    contoller.ModelState.AddModelError("FirstName", "The FirstName field is required."); 

    var result = contoller.Post(user); 

    Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); 
} 

Now, this is a good method to test the expected output, given a particular model validation failure occurs.
But, it doesn’t help check if an actual validation error occurred, given the parameter/model sent to the controller. ​

That brings us to the second method. ​

2. Using TryValidateModel

TryValidateModel(model) function actually validates the given model object.
But of course, you wouldn’t want to add a function to your controller just so as to make the unit test work.
Turns out, there is an even more neater/easier approach. Just call it directly from the unit test class.
​ ​

1
2
3
4
5
6
7
8
9
[TestMethod] 
public void TryValidateModelTest() 
{ 
    var controller = new UsersController(); 

    var user = GetUser(); 

    var result = controller.TryValidateModel(user); 
} 

​ Or, you could create a TestController class which takes User as the input parameter, and try it out, as this method only returns a boolean, saying if the model is valid or not.

But. Even so, you would be hit with a NullReferenceException, because the ObjectValidator of the controller class is null.

I got so lost on this part, I ended asking posting a question on StackOverflow.
And the solution would be to mock the ObjectValidator. And, a bit more 😐..

Note: I’m using moq framework​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

// Test Controller Class 
class TestController : Controller 
{ 
    delegate void ObjectValidatorDelegate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model); 

    private ObjectValidatorDelegate _objectValidatorDelegate; 

    public TestController() 
    { 
        _objectValidatorDelegate += ObjectValidatorExecutor; 

        var objectValidator = new Mock<IObjectModelValidator>(); 
        objectValidator.Setup(x => x.Validate(It.IsAny<ActionContext>(), 
                                It.IsAny<ValidationStateDictionary>(), 
                                It.IsAny<string>(), 
                                It.IsAny<object>())) 
            .Callback(_objectValidatorDelegate); 

        this.ObjectValidator = objectValidator.Object; 
    } 

    private void ObjectValidatorExecutor(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model) 
    { 
        var validationResults = new List<ValidationResult>(); 
        bool b = Validator.TryValidateObject(model, new ValidationContext(model), validationResults); 

        foreach (var result in validationResults) 
        { 
            ModelState.AddModelError(result.MemberNames.FirstOrDefault(), result.ErrorMessage); 
        } 
    } 

    [HttpPost] 
    public IActionResult Post(User value) 
    { 
        TryValidateModel(value); 

        if (!ModelState.IsValid) 
        { 
            return BadRequest(ModelState); 
        } 

        return Ok(); 
    } 
} 

// Unit Test Class 
[TestClass] 
public class UnitTest 
{ 
    [TestMethod] 
    public void TryValidateModelTest() 
    { 
        var controller = new TestController(); 
        var user = GetUser(); 

        var result = controller.Post(user); 

        Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); 
    } 
} 

​ This ☝, would validate the model. Of course, it doesn’t call the original controller, but hey, the intent is to validate the model, and voila, you have succesfully validated the model.

But, does it have to be this convoluted! Can’t it be made simpler?

Well, you’re in luck 👍, ‘cos you can. Which brings me to the third option. ​

3. Validator.TryValidateObject

​ From the above example, one thing is obvious. The actual model validation is done by Validator.TryValidateObject() function. Then why not just use it!
Well, that’s exactly what we are going to do. ​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestMethod] 
public void ValidatorModelStateValidationTest() 
{ 
    var result = new List<ValidationResult>(); 
    var contoller = new UsersController(); 

    var user = GetUser(); 

    var isValid = Validator.TryValidateObject(user, new ValidationContext(user), result); 

    Assert.IsFalse(isValid); 

    Assert.AreEqual(1, result.Count); 
    Assert.AreEqual("FirstName", result[0].MemberNames.ElementAt(0)); 
    Assert.AreEqual("The FirstName field is required.", result[0].ErrorMessage); 
} 

​ There it is. This makes the job very simple and neat.

You could also validate a single property of the model. ​

4. TryValidateProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestMethod] 
public void ValidatorModelStatePropertyValidationTest() 
{ 
    var result = new List<ValidationResult>(); 
    var contoller = new UsersController(); 

    var user = GetUser(); 

    var isValid = Validator.TryValidateProperty("this is a long name", new ValidationContext(new User()) { MemberName = "FirstName" }, result); 

    Assert.IsFalse(isValid); 

    Assert.AreEqual(1, result.Count); 
    Assert.AreEqual("FirstName", result[0].MemberNames.ElementAt(0)); 
    Assert.AreEqual("The field FirstName must be a string or array type with a maximum length of '10'.", result[0].ErrorMessage); 
}

Well. Happy unit testing

This post is licensed under CC BY 4.0 by the author.