C# Corner

Oh, CRUD … It's Test-Driven Development for ASP.NET MVC, Part 2

In this second part on TDD for ASP.NET MVC, Eric Vogel covers how to implement unit tests for the remaining CRUD controller actions.

Last time I wrote about test-driven development for ASP.NET MVC. Today I'll be covering how to unit test the controller layer of the application. Since the first installment, I've updated the Index action to use the Grid.Mvc.Ajax library, so be sure to download the code for this article.

To get started, install the Grid.Mvc.Ajax NuGet package. Next, open up the HomeControllerTest class. First, add a mock IAjaxGridFactory to the HomeControllerTest class:

private Mock<IAjaxGridFactory> _mockAjaxGridFactory; 

Then add the _partitionSize member variable that's used to test the Ajax Grid:

private const int _partitionSize = 10; 

Next, update the Index controller action unit test to expect that all contacts are loaded into the Ajax grid as an IQueryable<ContactViewModel> collection through the _mockAjaxGridFactory mock object, as in Listing 1.

Listing 1: _mockAjaxGridFactory Mock Object

[TestMethod]
public void Index_ExpectViewResultReturned()
{
  var stubContacts = (new List<Contact>
  {
    new Contact()
    {
      Id = 1,
      FirstName = "John",
      LastName = "Doe",
      Email = "[email protected]"
    },
    new Contact()
    {
      Id = 2,
      FirstName = "Jane",
      LastName = "Doe",
      Email = "[email protected]"
    }
  }).AsQueryable();
  var expectedGridRow = new ContactViewModel()
  {
    Id = stubContacts.ToList()[1].Id,
    Email = stubContacts.ToList()[1].Email,
    FirstName = stubContacts.ToList()[1].FirstName,
    LastName = stubContacts.ToList()[1].LastName
  };
  _mockContactService.Setup(x => x.GetAllContacts()).Returns(stubContacts);
  _mockAjaxGridFactory.Setup(
    x =>
      x.CreateAjaxGrid(It.Is<IQueryable<ContactViewModel>>(c => c.First() == expectedGridRow), 1,
        false, _partitionSize));

  var result = _controller.Index() as ViewResult;
}

Then update the Home controller Index action method in Listing 2 to make the unit test pass.

Listing 2: Updated Home Controller Index Action Method

 
[HttpGet]
public ActionResult Index()
{
  var contacts = _contactService.GetAllContacts();
  var model = new ContactIndexModel();
  var gridData = from contact in contacts
                 orderby contact.LastName, contact.FirstName
                 select new ContactViewModel()
                 {
                   Id = contact.Id,
                   Email = contact.Email,
                   FirstName = contact.FirstName,
                   LastName = contact.LastName
                 };
  var grid = _ajaxGridFactory.CreateAjaxGrid(gridData, 1, false, _partitionSize);
  model.Contacts = grid as AjaxGrid<ContactViewModel>;
  return View(model);
}

The Grid.Mvc.Ajax class also needs a controller action that returns the paged results for the contacts grid. Open the HomeController class and add the ContactsGrid action stub:

[HttpGet]
 public JsonResult ContactsGrid(int? page, bool? renderRowsOnly)
 {
   throw new NotImplementedException();
 }

Next, add the ContactsGrid unit test shown in Listing 3 that expects the results for the first page to be loaded from the Contact service into a collection of ContactViewModel with the mock AjaxGridFactory and returned as a JSON result.

Listing 3: ContactsGrid Unit Test

[TestMethod]
public void ContactsGrid_Given_page_and_renderRowsOnly_ExpectJsonResultReturned()
{
  int? page = 1;
  bool renderRowsOnly = false;
  var stubContacts = (new List<Contact>
  {
    new Contact()
    {
      Id = 1,
      FirstName = "John",
      LastName = "Doe",
      Email = "[email protected]"
    },
  }).AsQueryable();
  _mockContactService.Setup(x => x.GetAllContacts()).Returns(stubContacts);
  var expectedGridRow = new ContactViewModel()
  {
    Id = stubContacts.First().Id,
    Email = stubContacts.First().Email,
    FirstName = stubContacts.First().FirstName,
    LastName = stubContacts.First().LastName
  };
   var stubAjaxGrid = new Mock<IAjaxGrid>();
  _mockAjaxGridFactory.Setup(
    x =>
      x.CreateAjaxGrid(It.Is<IQueryable<ContactViewModel>>(c => c.First() == expectedGridRow), page.Value,
        renderRowsOnly, _partitionSize)).Returns(stubAjaxGrid.Object);
 string stubGridHtml = "grid";
 stubAjaxGrid.Setup(x => x.ToJson("_ContactsGrid", _controller)).Returns(stubGridHtml);
 bool hasItems = true;
 stubAjaxGrid.Setup(x => x.HasItems).Returns(hasItems);
 string expectedData = "{ Html = grid, HasItems = True }";

 var actual = _controller.ContactsGrid(page, renderRowsOnly);

 Assert.AreEqual(expectedData, actual.Data.ToString());
}

Now that the ContactsGrid action is tested, move on to the Create action. Open the HomeController class and add an empty Create action that has the HttpGet attribute:

[HttpGet]
public ActionResult Create()
{
  throw new NotImplementedException();
}

Now, add the unit test for the Create action that simply expects a view result with a new instance of a contact view model used:

[TestMethod]
public void Create_ExpectPartialViewResultReturned()
{
  var actual = _controller.Create() as ViewResult;
  var actualModel = actual.Model as ContactViewModel;
  Assert.IsNotNull(actualModel);
}

Next, implement the Create action to make the test pass:

[HttpGet]
public ActionResult Create()
{
  var model = new ContactViewModel();
  return PartialView("_Create", model);
}

The Create action simply creates a new ContactViewModel and returns the _Create view passing in the constructed model. Now it's time to stub out the Create form post action:

[HttpPost]
public ActionResult Create(ContactViewModel model)
{
  throw new NotImplementedException();
}

Then create the Create unit test in Listing 4 for a valid form model that should save the contact through the contact service and returns a JSON success result with the ID of the newly inserted item.

Listing 4: Creating the Create Unit Test

[TestMethod]
public void Create_Given_Valid_Model_ExpectRecordSavedAndJsonSuccessReturned()
{
  var model = new ContactViewModel()
  {
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
  };
  const int stubId = 1;
  _mockContactService.Setup(x => x.AddContact(It.Is<Contact>(c => c.FirstName == model.FirstName
                                                                  && c.LastName == model.LastName &&
                                                                  c.Email == model.Email))).Returns(stubId);
  const string expected = "{ Success = True, Object = 1 }";
  var actual = _controller.Create(model) as JsonResult;

  Assert.AreEqual(expected, actual.Data.ToString());
}

Now it's time to make the test pass by updating the Create form action in the Home controller class:

[HttpPost]
public ActionResult Create(ContactViewModel model)
{
  int id = _contactService.AddContact(new Contact()
    {
      FirstName = model.FirstName,
      LastName = model.LastName,
      Email = model.Email
    });
  return Json(new { Success = true, Object = id }, JsonRequestBehavior.AllowGet);
}

Next, I add a unit test to make sure the Create method handles invalid model state by returning the partial view with any errors displayed:

   
[TestMethod]
public void Create_Given_InvalidModelState_ExpectPartialResultReturned()
{
  var model = new ContactViewModel()
  {
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
  };
  _controller.ModelState.AddModelError("", "error");
  string expectedView = "_Create";

  var actual = _controller.Create(model) as PartialViewResult;

  Assert.AreEqual(expectedView, actual.ViewName);
  Assert.AreEqual(model, actual.Model);
}

Then, I update the Create form action to handle invalid model state to make the test pass:

[HttpPost]
public ActionResult Create(ContactViewModel model)
{
  if (ModelState.IsValid)
  {
    int id = _contactService.AddContact(new Contact()
    {
      FirstName = model.FirstName,
      LastName = model.LastName,
      Email = model.Email
    });
    return Json(new { Success = true, Object = id }, JsonRequestBehavior.AllowGet);
  }
  return PartialView("_Create", model);
}

Now, it's time to implement the edit functionality. First, stub out the Edit HTTP Get form action in the Home controller:

 
[HttpGet]
public ActionResult Edit(int id)
{
  throw new NotImplementedException();
 }

Then, create the unit test for the Edit Get action in Listing 5 that expects a partial view returned that's passed a loaded ContactViewModel, which is populated through the contact service.

Listing 5: Unit Test for Edit Get Action

[TestMethod]
public void Edit_Given_id_ExpectPartialViewResultReturned()
{
  int id = 1;
  string expectedView = "_Edit";
  var stubContact = new Contact()
  {
    Id = 1,
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
  };
  _mockContactService.Setup(x => x.GetContact(id)).Returns(stubContact);
  var expectedVm = new ContactViewModel()
  {
    Id = stubContact.Id,
    FirstName = stubContact.FirstName,
    LastName = stubContact.LastName,
    Email = stubContact.Email
  };

  var actual = _controller.Edit(id) as PartialViewResult;
  var actualVm = actual.Model as ContactViewModel;

  Assert.AreEqual(expectedView, actual.ViewName);
  Assert.AreEqual(expectedVm.Email, actualVm.Email);
  Assert.AreEqual(expectedVm.FirstName, actualVm.FirstName);
  Assert.AreEqual(expectedVm.Id, actualVm.Id);
  Assert.AreEqual(expectedVm.LastName, actualVm.LastName);
}

Then implement the Edit Get action, which uses the contact service to get a contact, load a view model and pass it to the _Edit partial view:

[HttpGet]
public ActionResult Edit(int id)
{
  var contact = _contactService.GetContact(id);
  var model = new ContactViewModel()
  {
    Id = contact.Id,
    FirstName = contact.FirstName,
    LastName = contact.LastName,
    Email = contact.Email
  };
  return PartialView("_Edit", model);
}

Next, add the stub for the Edit Put action, which will be used to update the contact model through the service:

[HttpPut]
public ActionResult Edit(ContactViewModel model)
{
  throw new NotImplementedException();
}

Then add the Edit controller action unit test in Listing 6 for a valid view model that expects the contact model to be updated through the contact service and a success JSON result returned.

Listing 6: Edit Controller Action Unit Test

[TestMethod]
public void Edit_Given_Valid_Model_ExpectModelUpdatedAndJsonSuccessReturned()
{
  var model = new ContactViewModel()
  {
    Id = 1,
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
  };
  _mockContactService.Setup(x => x.EditContact(It.Is<Contact>(c => c.Id == model.Id &&
                                              c.FirstName == model.FirstName
                                              && c.LastName == model.LastName &&
                                              c.Email == model.Email)));
  var expected = "{ Success = True }";
  var actual = _controller.Edit(model) as JsonResult;

  Assert.AreEqual(expected, actual.Data.ToString());
}

Next, update the Edit Put controller action to make the test pass:

[HttpPut]
public ActionResult Edit(ContactViewModel model)
{
  _contactService.EditContact(new Contact()
    {
      Id = model.Id,
      FirstName = model.FirstName,
      LastName = model.LastName,
      Email = model.Email
    });
  return Json(new { Success = true }, JsonRequestBehavior.AllowGet);
}

Then add the Edit Put unit test for an invalid model that expects the _Edit partial view retuned with the erroneous model state:

[TestMethod]
public void Edit_Given_InvalidModelState_ExpectPartialResultReturned()
{
  var model = new ContactViewModel()
  {
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
  };
  _controller.ModelState.AddModelError("", "error");
  string expectedView = "_Edit";

  var actual = _controller.Edit(model) as PartialViewResult;

  Assert.AreEqual(expectedView, actual.ViewName);
  Assert.AreEqual(model, actual.Model);
}

Next, update the Edit Put action to check for an invalid model state and return the _Edit partial view if the model state is invalid:

[HttpPut]
public ActionResult Edit(ContactViewModel model)
{
  if (ModelState.IsValid)
  {
    _contactService.EditContact(new Contact()
    {
      Id = model.Id,
      FirstName = model.FirstName,
      LastName = model.LastName,
      Email = model.Email
    });
    return Json(new { Success = true }, JsonRequestBehavior.AllowGet);
  }
  return PartialView("_Edit", model);
}

Then add the Delete Post controller action stub, which will be used to delete a contact record:

[HttpPost]
public JsonResult Delete(int id)
{
  throw new NotImplementedException();
}

Next, add the success Delete Post control action unit test, which expects the contact record to be deleted and a JSON success result returned:

[TestMethod]
public void Delete_Given_id_ExpectJsonSuccessReturned()
{
  int id = 1;
  _mockContactService.Setup(x => x.DeleteContact(id));
  const string expected = "{ Success = True }";
  var actual = _controller.Delete(id);

  Assert.AreEqual(expected, actual.Data.ToString());
}

Then update the Delete Post controller action to delete the contact record and record a JSON success result:

[HttpPost]
public JsonResult Delete(int id)
{
  _contactService.DeleteContact(id);
  return Json(new {Success = true}, JsonRequestBehavior.AllowGet);
}

Whew! That's how you unit test the controller layer of an MVC application. Stay tuned for the next installment: How to unit test the contact service class.

About the Author

Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube