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

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube