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

  • 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