C# Corner

Attain Code Management Nirvana via Test-Driven Development, Part 1

In this three-part series, we'll build an ASP.NET MVC application for managing a simple list of contacts, and in the process we'll show some of the direct benefits of test-driven development.

Test-driven development, or TDD is a method of software development that has become prevalent with on the onset of Agile development. TDD is a way of developing software in which you first create a test for your code before you create the implementation.

The main benefits of TDD are that you can quickly uncover design flaws early in development. Also, code that is easily testable tends to be easier to maintain because its specifications are the tests. Furthermore by having a good test suite, your code will become more maintainable because you can safely make changes to the code and you'll be sure that the existing functionality is still intact -- if you break something, you'll know because one or many tests will also fail.

I will be covering how to test an ASP.NET MVC application from the controller, model, and services levels in this three-part series. This is the approach I take when developing software. It is also known as top-down design: I start from the user interface and delve into the model then the services layer.

This time, I will focus on testing the controller layer. The full application will allow a user to manage a list of contacts. To get started, create a new ASP.NET MVC application in Visual Studio 2012 or 2013. Make sure to check the checkbox for Add unit tests, as seen in Figure 1.

New ASP.NET MVC Project with Unit Tests
[Click on image for larger view.] Figure 1. New ASP.NET MVC Project with Unit Tests

First I will be implanting the Index action on the Home controller. The Index action will return a list of contact view models to its view. The source contacts will be retrieved through an as yet uncreated interface called IContactService.

I like to put all of my service level classes into a new assembly. Create a new class library called VSMMvcTDD.Services. Next, add a reference to the Services project to the Web project. Then add a new file to the Service project called IContatService. Next add a new project to the solution named VSMMvcTDD.Entites. Then add a new class named Contact to the Entities project. Open up the Contact class and add properties named Id of type int, FirstName of type String, LastName of type String, and Email of type String as seen in Listing 1.

Listing 1: Contact.cs

using System;

namespace VSMMvcTDD.Entities
{
    public class Contact
    {
        public int Id { get; set; }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public String Email { get; set; }
    }
}

The next step is to create the ContactViewModel, add a new class named ContactViewModel to the MVC project's Models directory. Open up the ContactVideModel class and add properties named Id of type int, FirstName of type String, LastName of type String, and Email of type String as seen in Listing 2.

Listing 2: ContactViewModel.cs

using System;

namespace VSMMvcTDD.Models
{
    public class ContactViewModel
    {
        public int Id { get; set; }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public String Email { get; set; }
    }
}

Now that the base parts are in place we can create the Index Home controller unit tests, which will call the IContactService to retrieve all contacts and transfer them into contact view models for consumption by its view. Next add the Moq mocking library Nuget package to the VSMMvcTDD.Tests project, as seen in Figure 2.

Installing Moq Nuget Package
[Click on image for larger view.] Figure 2. Installing Moq Nuget Package

Moq will be used to mock out the IContactService in the HomeControllerTest unit test class. Then add references to the Entities and Services projects to the Tests project. Next open up the HomeControllerTest class file. Then add a private member variable named _mockContactService of type Mock<IContactService>:

private Mock<IContactService> _mockContactService;

Next I add a HomeController private member variable named _controller:

private HomeController _controller; 

Then I add the TestInitialize method, which runs before each unit test and initializes the mock contact service:

  [TestInitialize]
  public void TestInitialize()
  {
      _mockContactService = new Mock<IContactService>();
      _controller = new HomeController(_mockContactService.Object);
  }

Next I add the TestCleanup method, which runs after each unit tests and verifies any mock contact service setups:

[TestCleanup]
public void TestCleanup()
{
    _mockContactService.VerifyAll();
}

The unit test class will not compile yet because the HomeController doesn't contain a constructor that takes an IContactService instance, so let's create that now. This is one of the tenets of test-driven development: You don't create something until you need it. Open up the HomeController class and create a private member variable named _contactService of type IContactService:

private IContactService _contactService; 

Then initialize the _contactService variable in the HomeController constructor:

public HomeController(IContactService contactService)
 {
     _contactService = contactService;
 }

Next I implement the Index_ExpectViewResultReturned method in the HomeControllerTest class. First I stub out two test contacts into the stubContacts variable:

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]"
    }
};

Then I mock out a call to GetAllContacts method on the mock contact service, which returns the stubContacts:

_mockContactService.Setup(x => x.GetAllContacts()).Returns(stubContacts); 

The GetAllContacts method doesn't exist yet on IContactService so use Visual Studio to generate the method stub for you as seen in Figure 3.

GetAllContact Generate Stub in Visual Studio 2013
[Click on image for larger view.] Figure 3. GetAllContact Generate Stub in Visual Studio 2013

Next I create the expected view model from the stub contacts:

var expectedModel = new List<ContactViewModel>();
foreach (var stubContact in stubContacts)
{
    expectedModel.Add(new ContactViewModel()
    {
        Id = stubContact.Id,
        Email = stubContact.Email,
        FirstName = stubContact.FirstName,
        LastName = stubContact.LastName
    });
}

Then I call the Index action on the controller:

var result = _controller.Index() as ViewResult; 

Next I get the view model created from the Index action and cast it as a List<ContactViewModel>:

var actualModel = result.Model as List<ContactViewModel>; 

Lastly I loop over the expected contact view models and assert that the actual view models are the same:

for (int i = 0; i < expectedModel.Count; i++)
 {
     Assert.AreEqual(expectedModel[i].Id, actualModel[i].Id);
     Assert.AreEqual(expectedModel[i].Email, actualModel[i].Email);
     Assert.AreEqual(expectedModel[i].FirstName, actualModel[i].FirstName);
     Assert.AreEqual(expectedModel[i].LastName, actualModel[i].LastName);
 }

Your HomeControllerTest class should now look like Listing 3:

Listing 3: HomeControllerTest.cs

using System.Collections.Generic;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using VSMMvcTDD.Controllers;
using VSMMvcTDD.Entities;
using VSMMvcTDD.Models;
using VSMMvcTDD.Services;

namespace VSMMvcTDD.Tests.Controllers
{
    [TestClass]
    public class HomeControllerTest
    {
        private Mock<IContactService> _mockContactService;
        private HomeController _controller;

        [TestInitialize]
        public void TestInitialize()
        {
            _mockContactService = new Mock<IContactService>();
            _controller = new HomeController(_mockContactService.Object);
        }

        [TestCleanup]
        public void TestCleanup()
        {
            _mockContactService.VerifyAll();
        }
        
        [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]"
                }
            };
            _mockContactService.Setup(x => x.GetAllContacts()).Returns(stubContacts);
            var expectedModel = new List<ContactViewModel>();
            foreach (var stubContact in stubContacts)
            {
                expectedModel.Add(new ContactViewModel()
                {
                    Id = stubContact.Id,
                    Email = stubContact.Email,
                    FirstName = stubContact.FirstName,
                    LastName = stubContact.LastName
                });
            }

            var result = _controller.Index() as ViewResult;

            var actualModel = result.Model as List<ContactViewModel>;
            for (int i = 0; i < expectedModel.Count; i++)
            {
                Assert.AreEqual(expectedModel[i].Id, actualModel[i].Id);
                Assert.AreEqual(expectedModel[i].Email, actualModel[i].Email);
                Assert.AreEqual(expectedModel[i].FirstName, actualModel[i].FirstName);
                Assert.AreEqual(expectedModel[i].LastName, actualModel[i].LastName);
            }
        }
    }
}

Now run all of the unit tests and you should see the Index action test fail. You can run the unit test by going to TEST | Run | All Tests as seen in Figure 4.

Run All Unit Tests
[Click on image for larger view.] Figure 4. Run All Unit Tests

You should now see the failed test as seen in Figure 5.

Failed Index Action Unit Test
[Click on image for larger view.] Figure 5. Failed Index Action Unit Test

Now it's time to make the test pass. First a reference to the Entities project to the MVC project if you haven't already. Next, open up the HomeController class file. Then go to the Index action and first get all of the contacts from the contact service:

var contacts = _contactService.GetAllContacts();

Then create the list of contact view models from the contacts using LINQ:

var viewModel = new List<ContactViewModel>(from contact in contacts
     select new ContactViewModel()
     {
         Id = contact.Id,
         Email = contact.Email,
         FirstName = contact.FirstName,
         LastName = contact.LastName
     });

Lastly, return the view passing in the view model:

return View(viewModel); 

Your finished HomeController should now look like Listing 4.

Listing 4: Finished HomeController.cs

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using VSMMvcTDD.Models;
using VSMMvcTDD.Services;

namespace VSMMvcTDD.Controllers
{
    public class HomeController : Controller
    {
        private IContactService _contactService;

        public HomeController(IContactService contactService)
        {
            _contactService = contactService;
        }

        public ActionResult Index()
        {
            var contacts = _contactService.GetAllContacts();
            var viewModel = new List<ContactViewModel>(from contact in contacts
                select new ContactViewModel()
                {
                    Id = contact.Id,
                    Email = contact.Email,
                    FirstName = contact.FirstName,
                    LastName = contact.LastName
                });
            return View(viewModel);
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}

Now re-run the unit test and it should pass as seen in Figure 6.

Failed Index Action Unit Test
[Click on image for larger view.] Figure 6. Passing Index Action Unit Test

I've covered how to unit test a controller action in ASP.NET MVC using MSTest and Moq. Stay tuned to part 2, where I will continue covering the remaining implementation for create, update, and delete controller actions along with full unit tests.

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

  • 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.

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube