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 = "test@gmail.com"
     };

     _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 Sr. Software Developer at Kunz, Leigh, & Associates in Okemos, MI. 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 vogelvision@gmail.com.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.