I have a .NetCore MVC project and I'm trying to unit test my controller.
ViewModel (Note the [Required] attribute):
public class Bank : BaseObject
{
[Required]
[DisplayName("Bank")]
public string Name { get; set; }
}
Controller Action:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name")] Bank bank)
{
if (ModelState.IsValid)
{
await _bankService.Insert(bank);
return RedirectToAction(nameof(Index));
}
return View(bank);
}
Unit Test:
[Test]
public async Task When_PostingCreateBankThatIsInvalid_ThenBankIsReturned()
{
var bank = new Bank
{
//nothing set = invalid state - this is what we want
};
var controller = new BanksController(null);
var response = await controller.Create(bank);
}
I was expected the line if (ModelState.IsValid) to return false as Name is required - then I was going to perform my Asserts based on that. But the result is true and so we try to insert a bank.
What am I doing wrong here? I've Googled but I can only find answers that don't relate to unit tests. I thought the ModelState upheld the [Required] attribute?
If I test using the UI I am unable to create a bank without a name - it never even reaches the controller (as expected).
According to the docs:
Model validation occurs prior to each controller action being invoked
so I believe the problem lies with how I'm creating the BanksController. Am I approaching this test in the wrong manner? I wonder if I should just be setting the ModelState to invalid in the test...?
Attributes are metadata that is only recognized by the framework at run time and not during a unit test as they are actually read by the model binder that when the application is running.
For the state to change you will either have to run an integration test where the necessary parts of the framework are available to update the model state,
or update the model state manually since model binding isn't running (though an integration test would be used to exercise model binding) in the controller so that the test behaves as expected when being exercised.
[Test]
public async Task When_PostingCreateBankThatIsInvalid_ThenBankIsReturned() {
//Arrange
var bank = new Bank
{
//nothing set = invalid state - this is what we want
};
var controller = new BanksController(null);
controller.ModelState.AddModelError("Name","Name required");
//Act
var response = await controller.Create(bank);
//Assert
response.Should().NotBeNull()
.And.BeOfType<ViewResult>();
var viewResult = response as ViewResult;
viewResult.Model.Should().Be(model);
}
Don't try to test model validation or model binding in your unit tests - just test your action method's behavior when confronted with a particular ModelState value.
Reference Test controller logic in ASP.NET Core
Related
I am writing a unit test for a controller like this:
public HttpResponseMessage PostLogin(LoginModel model)
{
if (!ModelState.IsValid)
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
the model looks like:
public class LoginModel
{
[Required]
public string Username { set; get; }
[Required]
public string Password { set; get; }
}
Then I have unit test like this one:
[TestMethod]
public void TestLogin_InvalidModel()
{
AccountController controller = CreateAccountController();
...
var response = controller.PostLogin(new LoginModel() { });
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
}
Actually the ModelState is validated... which is weird for me as both fields are required...
Could anybody advise?
The reason the model state is valid is that a new model state is created when you new up a controller. Web API isn't doing the parameter binding for you here, so it doesn't even have a chance to add model state errors.
If you want to keep this as a unit test, then you should add the model state errors yourself and test what happens.
If you want to test that the model state would be invalid on a real request, I recommend you read this blog post:
http://blogs.msdn.com/b/youssefm/archive/2013/01/28/writing-tests-for-an-asp-net-webapi-service.aspx
and try testing against an in-memory server. One minor note for your case would be that you may want to use a StringContent instead of an ObjectContent on the request to make sure that Web API tries to deserialize and bind the body properly.
TL;DR
If you don't want to read the entire article provided by Youssef and want a quick solution to how to make ModelState.IsValid return false. Do this.
[TestMethod]
public void TestLogin_InvalidModel()
{
AccountController controller = CreateAccountController();
// new code added -->
controller.ModelState.AddModelError("fakeError", "fakeError");
// end of new code
...
var response = controller.PostLogin(new LoginModel() { });
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
}
Now I can imagine the CreateAccountController() looks something like this for minimum ->
return new AccountApiController()
{
Request = new HttpRequestMessage(),
Configuration = new HttpConfiguration()
};
Hope this gives a quick answer for those googling :)
As mentioned before, you need integration tests to validate the ModelState. So, with Asp.Net Core, I'm digging this question to add a simple solution for integrating tests with Asp.Net Core and validation of ModelState
Add the package Microsoft.AspNetCore.TestHost and you can submit requests this simple:
var server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
var client = server.CreateClient();
var model = new { Name = String.Empty };
var content = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
var result = await client.PostAsync("/api/yourApiEndpoint", content);
result.StatusCode.Should().Be(HttpStatusCode.BadRequest);
You can find more about it here:
http://asp.net-hacker.rocks/2017/09/27/testing-aspnetcore.html
Hope it helps.
I used the following to validate the model state in unit test Visual studio 2017, C#, NET 4.x.x
[TestMethod]
public void TestYourValidationModel()
{
var configuration = new HttpConfiguration();
configuration.Filters.Add(new ValidateModelAttribute());
// Get the quote
var controller = new YourController
{
Request = new HttpRequestMessage(),
Configuration = configuration
};
var request = YourRequestObject;
controller.Request.Content = new ObjectContent<YourRequestType>(
request, new JsonMediaTypeFormatter(), "application/json");
controller.Validate(request);
Assert.IsTrue(controller.ModelState.IsValid, "This must be valid");
}
The example is for a request in JSON format. Substitute YourController for the name of your controller, and YourRequesType, for the object type of your request.
This give you the option to test your model for validation without go to the service.
I could not get response.StatusCode property on the response object. So I had to assert as below, which works good for me.
IHttpActionResult actionResult = controller.ActionMethod(request);
Assert.IsInstanceOf(actionResult);
Is there a way to check status code on the response object ?
Like for example.
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
Even though there are couple of Posts on StackOverflow about Unit Testing Action Result in MVC, I have a specific Question ....
Here is my ActionResult in Controller:
public ActionResult Index()
{
return View(db.Products.ToList());
}
Every Item in Products has different attributes like Name,Photo,Quantity etc..
I wrote a testmethod for this method .It looks as follows :
private CartEntity db = new CartEntity();
[TestMethod]
public void Test_Index()
{
//Arrange
ProductsController prodController = new ProductsController();
ViewResult = prodController.Index();
}
What Should I compare in this case since there are no parameters are being passed into Index Action
Check out the ViewResult class, this can show you what else you could test.
What you need to do is mock your DbContext and supply it with data in the Products property (DbSet<>) as this is being called in your controller's action.
You can then test
The type being returned
The model on the ViewResult
The ViewName which should be empty or Index
Sample code
[TestMethod]
public void Test_Index()
{
//Arrange
ProductsController prodController = new ProductsController(); // you should mock your DbContext and pass that in
// Act
var result = prodController.Index() as ViewResult;
// Assert
Assert.IsNotNull(result);
Assert.IsNotNull(result.Model); // add additional checks on the Model
Assert.IsTrue(string.IsNullOrEmpty(result.ViewName) || result.ViewName == "Index");
}
If you need help mocking a DbContext there are existing frameworks and articles on this subject. Here is one from Microsoft titled Testing with a mocking framework. Ideally you should be injecting your dependencies (including DbContext instances) into the constructors of your Controller instances using a DI framework like AutoFac or Unity or NInject (the list goes on). This also makes unit testing much easier.
I created an ASP.NET MVC 5 application, with the following model class using a globalized Required validation attribute on its Email property:
public class Person
{
[Required(ErrorMessageResourceType = typeof(Resources),
ErrorMessageResourceName = "EmptyEmailError")]
public string Email { get; set; }
}
The Create POST action method of the PersonController class is:
[HttpPost]
public ActionResult Create(FormCollection formData)
{
Person newPerson = new Person();
UpdateModel(newPerson, formData);
if (ModelState.IsValid) {
// code to create the new person
return View("Index", newPerson);
}
else {
return View();
}
}
And the test case for verifying that a person with an empty e-mail address is not created is:
[TestMethod]
void Create_DoesNotCreatePerson_WhenEmailIsEmpty()
{
// arrange
FormCollection person = new FormCollection() { { "Email", string.Empty } };
PersonController controller = new PersonController();
controller.ControllerContext = new ControllerContext();
// act
controller.Create(person);
// assert
Assert.IsFalse(controller.ModelState.IsValid);
}
With this code the normal execution works fine. However, the test doesn't pass, which means that UpdateModel is returning true and thus not validating the person model correctly. The TryUpdateModelmethod doesn't work too.
But if I remove the ErrorMessage arguments from the Required validation attribute in the Person class (i.e. leaving it just as [Required]), then the test passes.
So I don't know why UpdateModel doesn't validate the globalized Person model when I call the Create action method from the test case.
I'm happy to see you are writing unit tests for your code :)
First, why don't you just pass Person in the first place, do you really need FormCollection?
[TestMethod]
void Create_DoesNotCreatePerson_WhenEmailIsEmpty()
{
// arrange
FormCollection person = new FormCollection() { { "Email", string.Empty } };
PersonController controller = new PersonController();
controller.ControllerContext = new ControllerContext();
// third, you might need to add this (haven't tested this)
controller.ModelState.AddModelError("Email", "fakeError");
// act
controller.Create(person);
// assert
Assert.IsFalse(controller.ModelState.IsValid);
}
Fourth and most important. I dont' think you should test like this. You want to test the outcome for different scenarios. That means you want to test the return value for the method. When you say Assert.IsFalse(controller.ModelState.IsValid); you basically say you don't trust Microsoft's code and you want to test their code. I can assure you they have unit tests on their code and you do not need to test it again ;)
Instead, test what view is returned and what object is returned from the action result. Here is a complete article for how to unit test this -> https://msdn.microsoft.com/en-us/library/gg416511(VS.98).aspx (See "Creating a test for retrieving contacts"). Hope this helps!
EDIT (Answer to comments):
Sory for the long response time.
I removed to "second".
If you want to to test the real production code, you want to do integration tests, not unit tests. If unit tests is what you want I recommend you to follow my solution proposal, otherwise create integration tests. I don't know how to convince you more then I've tried so read more and from several sources and I think you will agree later on how to do unit testing in this case :)
Good luck!
My Model class is as follow :
public class PostInputViewModel
{
[Required]
[MinLength(1)]
[MaxLength(125)]
public string Title { get; set; }
[Required]
[AllowHtml]
[Display(Name="Content")]
public string Content { get; set; }
}
and controller is as follow :
[HttpPost]
public ActionResult Write(PostInputViewModel input)
{
if (!ModelState.IsValid)
return View(input);
var post = new Post
{
Title = input.Title,
Content = input.Content,
DateCreated = DateTime.Now,
DateModified = DateTime.MaxValue,
};
dbContext.Posts.Add(post);
dbContext.SaveChanges();
return RedirectToAction("Index", "Home");
}
When I run web application by clicking F5, and if I don't input title and content value, ModelState.IsValid is false, However if I test controller class with unit test case, ModelState.IsValid is always true. The test case is as follow :
[TestMethod]
public void write_should_validate_model()
{
var input = new PostInputViewModel();
input.Title = null;
input.Content = null;
var actionResult = controller.Write(input) as ViewResult;
Assert.IsFalse(actionResult.ViewData.ModelState.IsValid);
}
Am I missing something?
Thanks in advance.
If you want to have your controller try to validate the model, you can call the TryValidateModel method before your assert:
controller.TryValidateModel(input);
But I agree that you'd really only be testing the validation attributes. It might be OK, though; it would validate that your model has the expected attributes applied.
Validation of model doesn't happen in the Controller. It happens before the model is passed to the controller.
Notice the controller action is only 'testing' whether the model is valid. Who is validating the model in your test case? Nothing!
You can do the validation using Validator class of .NET however in that case you would be testing .NET validation. This is one of the common mistakes people make while writing unit tests. They test 3rd party code instead of their own.
If you really want to test that you have applied the correct validation attributes to the class then you can simply reflect your class and check for attributes on properties. This way you would be skipping the .NET validation layer and your test will only fail if you miss an attribute.
Validation actually happens before the Write method on your controller is called, which populates the ModelState property.
Your unit test isn't really testing the controller in my opinion (if that is in fact what you're trying to do).
A true controller test would look something like this:
[TestMethod]
public void write_should_validate_model()
{
controller.ModelState.AddModelError("Title", "Empty"); //These values don't really matter
var actionResult = controller.Write(new PostInputViewModel()) as ViewResult;
//Assert that the correct view was returned i.e. Not Home/Index
//Assert that a new post has not been added to your Mock Repository
}
In my unit tests, I find that when I return from a controller action using View() with no view name, ViewResult.ViewName is set to string.Empty. In order for this to be set, it has to be specified as a parameter to the View() call. For example, given the following unit test:
[TextFixture]
public class MyControllerTests
{
[Test]
public void TestMyAction()
{
var controller = new MyController();
var result = controller.MyAction();
Assert.AreEqual("MyAction", result.ViewName);
}
}
The following action implementation will cause the unit test to fail:
public class MyController : Controller
{
public ActionResult MyAction()
{
return View();
}
}
whilst this one will pass:
public class MyController : Controller
{
public ActionResult MyAction()
{
return View("MyAction");
}
}
I'm using ASP.NET MVC 2 (pre-beta) on .NET 4.0. I'm not using anything .NET 4.0-specific, however. I find this behaviour odd because I had thought that the ViewName was one of the reliable properties that could be checked in the unit tests' assertions.
This is a well known "feature" of ASP.NET MVC. Microsoft has documented it since the first version...
When no explicit view name is specified, the framework is trying to find one based on conventions (in "Views\controllername\actionname" or "Shared\controllername\actionname"). ViewName is only relevant if you want to deviate from that convention. So your unit test makes false assumptions.
HTH.