Monday, February 28, 2011

Replace code comments with unit tests

Not long ago code comments using XML was the thing. Every class and method should have some comment related to it. This worked great in the beginning until the code was updated and the comments were not. Eventually it all got out of sync and the comments were considered useless and were ignored.

The new trend today is to see unit tests as the documentation as they are (usually) up-to-date. By just reading the method names of the tests we should get the idea of what the code is actually doing. Consider the following method:

public void UpdateCustomerName(int customerId, string name, string email)
{
 if (string.IsNullOrEmpty(name))
  throw new ValidationException("Customer name cannot be empty.");

 _dataProvider.UpdateCustomerName(customerId, name);

 if (!string.IsNullOrEmpty(email))
  _mailSender.SendMail(email, "Your customer data has been updated.");
}

With the following unit tests:

[TestMethod]
[ExpectedException(typeof(ValidationException))]
public void UpdateCustomer_throw_exception_when_customer_name_is_empty()
{
 var dataProviderFake = MockRepository.GenerateStub<IDataProvider>();
 var mailSenderFake = MockRepository.GenerateStub<IMailSender>();

 var customerUpdater = new CustomerUpdater(dataProviderFake, mailSenderFake);

 customerUpdater.UpdateCustomerName(1, "", "");
}

[TestMethod]
public void UpdateCustomer_update_the_customer_name_in_the_database()
{
 var dataProviderFake = MockRepository.GenerateStub<IDataProvider>();
 var mailSenderFake = MockRepository.GenerateStub<IMailSender>();

 var customerUpdater = new CustomerUpdater(dataProviderFake, mailSenderFake);

 customerUpdater.UpdateCustomerName(1, "Bill", "bill@somecompany.com");

 dataProviderFake.AssertWasCalled(x => x.UpdateCustomerName(0, null), x => x.IgnoreArguments());
}

[TestMethod]
public void UpdateCustomer_email_customer_if_email_address_is_not_empty()
{
 var dataProviderFake = MockRepository.GenerateStub<IDataProvider>();
 var mailSenderFake = MockRepository.GenerateStub<IMailSender>();

 var customerUpdater = new CustomerUpdater(dataProviderFake, mailSenderFake);

 customerUpdater.UpdateCustomerName(1, "Bill", "bill@somecompany.com");

 mailSenderFake.AssertWasCalled(x => x.SendMail(null, null), x => x.IgnoreArguments());
}

By reading the method names we see the main logic of this method:
1. Throw exception when the customer name is empty.
2. Update the customer's name in the database.
3. E-mail customer if the e-mail address is not empty.

Conclusion: If we focus on writing unit tests with good method names then we can save ourselves from writing inline code comments.