C# Corner

TDD for ASP.NET MVC, Part 3: Contact Service Class

In this final part on test-driven app development with ASP.NET MVC, Eric covers how to unit test the services layer.

Thanks for reading my series on test-driven development for ASP.NET MVC. If you're just joining us, read parts 1 and 2 first. This time, I'm how to unit test the contact service class that creates, reads, updates, and deletes contact database records through Entity Framework.

To get started create a new unit test project named VSMMvcTDD.Services.Tests. Next add the Moq NuGet package to the service test project. Then create a new Basic Unit Test class named ContactServiceTests. First I mock out the ContactConttext:

private Mock<ContactContext> _mockContactContext;

Then I mock out the Contacts collection in the ContactContext class:

private Mock<DbSet<Contact>> _mockContacts;

Next I create the ContactService that will be tested:

private ContactService _contactService;

Then I add the TestInitialize method, which is executed before each unit test that initializes the contact service instance to test:

  [TestInitialize]
  public void TestInitialize()
  {
      _mockContactContext = new Mock<ContactContext>();
      _mockContacts = new Mock<DbSet<Contact>>();
      _mockContactContext.Setup(x => x.Contacts).Returns(_mockContacts.Object);
      _contactService = new ContactService(_mockContactContext.Object);
  }

Next I add the TestCleanup method that runs after each unit tests that verifies the setups for the contact context mock:

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

Then I add the unit test for the GetAllContacts method that expects all of the contacts in the Contacts collection of the ContactContext to be returned:

[TestMethod]
public void GetAllContacts_ExpectAllContactsReturned()
{
    var stubData = (new List<Contact>
    {
        new Contact()
        {
            Id = 1,
            FirstName = "John",
            LastName = "Doe"
        },
        new Contact()
        {
            Id = 2,
            FirstName = "Jane",
            LastName = "Doe"
        }
    }).AsQueryable();
    SetupTestData(stubData, _mockContacts);

    var actual = _contactService.GetAllContacts();

    CollectionAssert.AreEqual(stubData.ToList(), actual.ToList());
}

To implement the SetupTestData method that mocks the given mock DbSet to return the given test data, use this:

private void SetupTestData<T>(IQueryable<T> testData, Mock<DbSet<T>> mockDbSet) where T : class
 {
     mockDbSet.As<IQueryable<Contact>>().Setup(m => m.Provider).Returns(testData.Provider);
     mockDbSet.As<IQueryable<Contact>>().Setup(m => m.Expression).Returns(testData.Expression);
     mockDbSet.As<IQueryable<Contact>>().Setup(m => m.ElementType).Returns(testData.ElementType);
     mockDbSet.As<IQueryable<Contact>>().Setup(m => m.GetEnumerator())
         .Returns((IEnumerator<Contact>) testData.GetEnumerator());
 }

Then it's just a matter of updating the ContactService class to use the ContactContext:

private readonly ContactContext _contactContext;
public ContactService(ContactContext contactContext)
{
    _contactContext = contactContext;
}

To make the unit test pass, I need to implement the GetAllContacts method:

public IQueryable<Contact> GetAllContacts()
{
    return _contactContext.Contacts;
}

The AddContact unit test expects the given contact record to be inserted and its ID returned. To do that, I add the following:

  [TestMethod]
  public void AddContact_Given_contact_ExpectContactAdded()
  {
      var contact = new Contact()
      {
          FirstName = "John",
          LastName = "Doe"
      };
      const int expectedId = 1;
      _mockContactContext.Setup(x => x.SaveChanges()).Callback(() => contact.Id = expectedId);

      int id = _contactService.AddContact(contact);

      _mockContacts.Verify(x => x.Add(contact), Times.Once);
      _mockContactContext.Verify(x => x.SaveChanges(), Times.Once);
      Assert.AreEqual(expectedId, id);
  }

To insert the record and return its new ID, I update the AddContact method this way:

public int AddContact(Contact contact)
 {
     _contactContext.Contacts.Add(contact);
     _contactContext.SaveChanges();
     return contact.Id;
 }

Then I add the GetContact unit test that expects the contact record for the given ID to be returned:

[TestMethod]
public void GetContact_Given_id_ExpectContactReturned()
{
    int id = 2;
    var stubData = (new List<Contact>
    {
        new Contact()
        {
            Id = 1,
            FirstName = "John",
            LastName = "Doe"
        },
        new Contact()
        {
            Id = 2,
            FirstName = "Jane",
            LastName = "Doe"
        }
    }).AsQueryable();
    SetupTestData(stubData, _mockContacts);

    var actual = _contactService.GetContact(id);

    Assert.AreEqual(stubData.ToList()[1], actual);
}

Next I implement the GetContact method to make the unit test pass:

public Contact GetContact(int id)
 {
     return _contactContext.Contacts.SingleOrDefault(c => c.Id == id);
 }

Then I add the unit test for the EditContact method that accepts a contact and updates the record through Entity Framework:

[TestMethod]
 public void EditContact_Given_contact_ExpectExistingContactUpdated()
 {
     var stubData = (new List<Contact>
     {
         new Contact()
         {
             Id = 1,
             FirstName = "John",
             LastName = "Doe"
         },
         new Contact()
         {
             Id = 2,
             FirstName = "Jane",
             LastName = "Doe"
         }
     }).AsQueryable();
     SetupTestData(stubData, _mockContacts);
     var contact = new Contact()
     {
         Id = 1,
         FirstName = "Ted",
         LastName = "Smith",
         Email = "[email protected]"
     };

     _contactService.EditContact(contact);
     var actualContact = _mockContacts.Object.First();
     
     Assert.AreEqual(contact.Id, actualContact.Id);
     Assert.AreEqual(contact.FirstName, actualContact.FirstName);
     Assert.AreEqual(contact.LastName, actualContact.LastName);
     Assert.AreEqual(contact.Email, actualContact.Email);
     _mockContactContext.Verify(x => x.SaveChanges(), Times.Once);
 }

To update the existing contact record, I update the EditContact method like this:

public void EditContact(Contact contact)
 {
     var existing = GetContact(contact.Id);
     existing.FirstName = contact.FirstName;
     existing.LastName = contact.LastName;
     existing.Email = contact.Email;
     _contactContext.SaveChanges();
 }

Then I add the DeleteContact unit test that finds the record with the given ID and removes it:

[TestMethod]
 public void DeleteContact_Given_id_ExpectContactDeleted()
 {
     var stubData = (new List<Contact>
     {
         new Contact()
         {
             Id = 1,
             FirstName = "John",
             LastName = "Doe"
         },
         new Contact()
         {
             Id = 2,
             FirstName = "Jane",
             LastName = "Doe"
         }
     }).AsQueryable();
     SetupTestData(stubData, _mockContacts);
     var contact = stubData.First();

     _contactService.DeleteContact(1);

     _mockContacts.Verify(x => x.Remove(contact), Times.Once);
     _mockContactContext.Verify(x => x.SaveChanges(), Times.Once);
 }

Lastly I implement the DeleteContact method that finds the record by the given ID and deletes it:

public void DeleteContact(int id)
 {
     var existing = GetContact(id);
     _contactContext.Contacts.Remove(existing);
     _contactContext.SaveChanges();
 }

And that is how unit testing of the service layer of an ASP.NET MVC application is done. Next time, I'll show how to unit test view model validation.

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