In-Depth
Interface-Based Programming in C#
Interfaces can make your applications easier to test and maintain.
- By Patrick Steele
- January 1, 2010
Interfaces help define a contract, or agreement, between your application and other objects. This agreement indicates what sort of methods, properties and events are exposed by an object. But interfaces can be utilized for much more. Here, we'll look at how interfaces can help make your applications and components easier to test.
Using Interfaces in Your Design
Interfaces are often used when you have different types of objects that you want to perform a similar operation on. The System.IComparable interface in the .NET Framework is a good example. Object comparison is common in programming. Microsoft can't know every way you might compare objects, so it defined an interface for this purpose. Using interfaces, we can write code that works against the interface implemented by different objects--but we shouldn't reserve interfaces just for defining common attributes or behavior.
Suppose we're writing an application that lets users register for a conference. Once they've entered their personal information, there are a number of steps we'll need to take to get them registered:
- Calculate their fees. Some individuals may have a code that gets them in for free. There are other discounts available based on when you register like early-bird discounts and so forth.
- Bill their credit card. This example only accepts credit cards for payment.
- Save their information to a SQL Server database.
- Generate and send a confirmation e-mail.
If we look at a visual diagram to show how this all fits together, it might look something like Figure 1. Look at an extremely simple implementation of step 1, the fee calculator:
public class FeeCalculator
{
private readonly DateTime conferenceDate = new
DateTime(2010, 2, 1);
public decimal CalculateFee()
{
if ((conferenceDate - DateTime.Now).Days >= 30)
{
return 100;
}
return 160;
}
}
[Click on image for larger view.] |
Figure 1. Components for a conference-registration service. |
If the user signs up at least 30 days before the conference, it will only cost them $100. If it's less than 30 days before the conference, it will cost them $160. Register early.
We have a single class that implements our fee calculator. We don't need multiple fee calculators. Why define an interface just for calculating fees? We're also only going to be saving to a single type of database-SQL Server. How can this example be used to show the benefits of interfaces? The answer: Testability.
Using Interfaces to Isolate Testing
We need to write unit tests for our registration service. When we test it, we want to make sure we test just the registration service and not the fee calculator, payment processor and so on. If we use the real implementations of those other objects, our RegistrationService constructor may look something like this:
public class RegistrationService
{
public RegistrationService(
FeeCalculator feeCalculator,
PaymentProcessor
paymentProcessor,
SQLRepository sqlRepository,
EmailGenerator emailGenerator,
EmailSender emailSender)
{
}
}
[Click on image for larger view.] |
Figure 2. Isolating the registration service for testing. |
During testing, we'd call our real FeeCalculator, and we'd hit a real database and other resources. This makes testing more difficult. Problems in the fee calculator service will impact our registration service testing. If we're hitting a real database, we'll have to initialize the database to a known state; make sure SQL Server is installed; and worry about timeouts and details unrelated to our registration service. The coupling of the service with the components is too tight. Interfaces can isolate components we'd like to test.
Let's start with our FeeCalculator. All it needs to do is contain a method called CalculateFee that returns a decimal value. We'll create an interface to define how a FeeCalculator works:
public interface IFeeCalculator
{
decimal CalculateFee();
}
Our earlier FeeCalculator can be updated to state that it implements this interface:
public class FeeCalculator : IFeeCalculator
{
...
}
Now we define interfaces for all our other dependant objects. Then we'll update the RegistrationService constructor so it'll accept the interfaces instead of the actual implementation objects:
public class RegistrationService
{
public RegistrationService(
IFeeCalculator feeCalculator,
IPaymentProcessor paymentProcessor,
ISQLRepsoitory sqlRepository,
IEmailGenerator emailGenerator,
IEmailSender emailSender)
{
}
}
Now you're asking, "If we don't want to use the real implementations of those objects, what are we supposed to use during testing?" The answer is mock objects.
Using Mock Objects in Your Unit Tests
A mock object, sometimes referred to as a "fake" or "stub," is simply an object that mimics the behavior of a real object. The advantage we get with mock objects, or "mocks," is that they allow us to swap out the real implementation on an object for an implementation that we can control to produce specific results.
If we go back to our FeeCalculator example, the fee returned by the CalculateFee method is dependent on the current date. If we were to use the real object during testing, the result of CalculateFee could change depending on the date we run the test. This doesn't make for a very robust unit test. Instead, a mock IFeeCalculator can return what we want:
public class EarlyBirdFeeCalculator : IFeeCalculator
{
public decimal CalculateFee()
{
return 100;
}
}
public class ProcrastinatorFeeCalculator :
IFeeCalculator
{
public decimal CalculateFee()
{
return 200;
}
}
These two classes are part of our unit test assembly. They define fee calculators that return the two types of fees we need to use in our registration service. Whenever we want to test some code that will involve the user registering 30 days before the conference date, we'll use the EarlyBirdFeeCalculator. Likewise, any tests that need to assess those who register within 30 days of the conference will use the ProcrastinatorFeeCalculator. Notice that the procrastinator fee is different from the real FeeCalculator. The value doesn't matter here because it's just a mock. What's important is that we'll know-during our testing-that when we use the ProcrastinatorFeeCalculator, the CalculateFee method will always return 200.
Mock objects can be used for more than just a canned response. We can use them to ensure certain calls have been made. For example, let's create a Mock IEmailSender to make sure that the Send method on IEmailSender was called. Assuming the following interface:
public interface IEmailSender
{
void SendMail(MailMessage message);
}
We can create our mock as:
public class ExpectedSendEmailSender : IEmailSender
{
private bool sendCalled = false;
public void SendMail(MailMessage message)
{
sendCalled = true;
}
public bool SendCalled
{
get { return sendCalled; }
}
}
In unit tests, we can create an instance of ExpectedSendEmailSender and pass it into our RegistrationService. After we've called our method to register a user, we can make sure the SendCalled property is true. If someone were to edit the code that handles registering a user-and accidentally delete the line of code that calls IEmailSender.Send-our unit test would fail since SendCalled would be false.
This pattern of creating mock objects can be used for all the other interfaces as well:
- IPaymentProcessor
- IConferenceRepository
- IEmailGenerator
- IEmailSender
The benefits we get are huge. Here are just a few:
- By using a mock payment processor, we're not making a bunch of Web calls into our payment gateway-and possibly running up fees because of the high number of transactions. We also don't need an Internet connection to run the tests.
- A mock conference repository means we don't need a real SQL Server repository set up. Any methods on the interface that are used to query the database would return a canned response of pre-populated data.
- Imagine our IEmailSender being the real implementation that used SMTP to send real e-mails. We'd again require an Internet connection, or at least a local SMTP server; all of which would add complexity to our unit tests.
When we consider our original diagram, we can see how we'll isolate the testing of our registration service by employing mocks. The downside of this approach is writing and maintaining all of that mock code. This is where a mocking framework comes in handy.
Using Rhino.Mocks
I'll use Rhino.Mocks as my mock framework in these examples. It's free, open source and can automate the creation of our mocks and stubs. Other frameworks exist and behave similarly, so the concepts shown here will work with any of the frameworks.
From an academic standpoint, there isn't much difference between a mock and a stub, but Rhino.Mocks makes a small distinction.
Stubs are used for returning a canned response. The example we used earlier with our EarlyBirdFeeCalculator and ProcrastinatorFeeCalculator is a perfect example of a stub. Mocks can provide canned responses as well, but they can also have expectations set. The example shown earlier of making sure the SendMail method of IEmailSender was called is an example of where you'd use a mock. You would tell Rhino.Mocks that you expect the SendMail method to be called. Later, after the test is run, you'd ask Rhino.Mocks to verify that your expectation was met.
In a nutshell, stubs and mocks are almost identical. Both allow you to return a specific response from method calls. The key difference is that you can only set expectations on a mock.
Let's get back to our unit tests of our RegistrationService. When we want to use our EarlyBirdFeeCalculator or our ProcrastinatorFeeCalculator, we'd simply create a new instance of them in our unit test code. The FeeCalculator we created would be passed to the RegistrationService constructor, and we'd write our test code knowing that the call to CalculateFee will return a known value. Let's use Rhino.Mocks to eliminate the need for a separate class.We'll remove the EarlyBirdFeeCalculator class and instead create a stub:
IFeeCalculator feeCalculator =
MockRepository.GenerateStub<IFeeCalculator>();
feeCalculator.Stub(f =>
f.CalculateFee()).Return(100);
What we're doing here is asking Rhino.Mocks to create a stubbed implementation of our IFeeCalculator interface. Then, using the stub extension method, we use a lambda expression to indicate exactly what we want the CalculateFee method on our IFeeCalculator to return. Our seven lines of code to define the EarlyBirdFeeCalculator have been reduced to two. And we don't need a separate class for the two different types of fees.
In addition to maintaining less code, we can see in our unit test code what the value returned by CalculateFee is. We don't have to jump over to the other class definition to see what we'd defined for the value of CalculateFee-it's right in front of us. This makes the readability and maintainability of our unit tests much easier.
What about the case of setting expectations? For that, we need to use a mock. The code for creating a mock in Rhino.Mocks is simple:
IEmailSender emailSender =
MockRepository.GenerateMock<IEmailSender>();
Now we can set an expectation that we expect the SendMail method to be called. Again, we'll use an extension method and a lambdA:
emailSender.Expect(e => e.SendMail(null))
.IgnoreArguments();
When you express a method call in a lambda, you must provide arguments for the method call. In this case, we can't specify the exact MailMessage object that'll be passed to SendMail-the MailMessage object will be constructed somewhere inside our RegistrationService class. Therefore, we simply supply a null argument and tell Rhino.Mocks to ignore the arguments passed to the SendMail method. Without the IgnoreArguments call, Rhino.Mocks would be expecting a call to SendMail with a null MailMessage object. We don't want that, so we tell Rhino.Mocks to only look for a call to SendMail and ignore the differences in argument values between the lambda and the actual call made during the test.
The last thing to do with expectations is verify your expectations were met. This is called after executing the method you want to test:
emailSender.VerifyAllExpectations();
The above code will throw an exception if any expectations defined via "Expect" calls on the mocked object were not met. For our example we set up a single expectation on SendMail. If that's never called, your unit test will fail because VerifyAllExpectations will throw an exception. What if you need to set both an expectation and a canned response? You can set a return value on an expectation just like you did on a stub. Here's an example of expecting a call on IPaymentProcess.Process and always returning true:
IPaymentProcessor pp = MockRepository.
GenerateMock<IPaymentProcessor>();
pp.Expect(p => p.Process()).Return(true);
This allows you to easily create a test to check both success and failure processing of your registration payments.
Exception Handling
Every application has to deal with exceptions. Testing your exception handling code can be tricky since exceptions aren't supposed to happen. Fortunately, Rhino.Mocks makes testing your exception handling easier. Instead of telling Rhino.Mocks that you want a method to return a specific result, you can actually have it run specific code when the method is called. The "Do" extension method takes in a delegate that'll be executed whenever your stubbed method is called.
Suppose you want to test your exception handling that handles missing files. It's as easy as:
public delegate void ThrowExceptionDelegate();
IFileReader reader =
MockRepository.GenerateStub<IFileReader>();
reader.Stub(r => r.ReadFile()).Do(
new ThrowExceptionDelegate(delegate()
{ throw new FileNotFoundException(); }
));
When your test code calls the ReadFile method, a FileNotFoundException will be thrown, and you can ensure your exception handling code is working properly.
Interfaces can help you create a more loosely coupled architecture. That, along with a mocking framework, can make your unit tests more robust and maintainable.