Recently a colleague of mine, which is a quite experienced programmer, wants to write his first code using TDD. It takes only minutes until he encountered the first obstacle.
It seems that TDD is quite difficult for beginners. I will describe some of the problems and show code examples to help TDD beginners to become familiar faster with this excellent technique. Based on these kinds of experiences I described also a pattern called
Own Library Interface which is applicable in the situation described below.
My colleague needs to implement a background service, which monitors a database. The background service has to send notifications to one or more external receivers. He designed an EmailSender class, which can be initialized with all email related information like the smtp server, the sender and the receiver email addresses. Now he wants to write the SendToAll() method, which only needs some information for the receivers:
public void SendToAll(string subject, string details)
He has already poked around in the .NET framework and has found the SmtpClient class for easy sending of emails. He wants to use this class in his own EmailSender class. He knows that he first has to write a test, if he wants to follow the TDD principles, before he can begin with the implementation.
He starts to implement the test method, which should verify the successful execution of the method:
[TestFixture]
public class EmailSenderFixture
{
[Test]
public void SendToAll()
{
// Setup
EmailSender sender = new EmailSender();
...
// Exercise
sender.SendToAll("TestSubject","TestDetails");
// Verify
// ?
}
}
How can he verify the outcome of the SendToAll method? One solution would be to let his own class send emails to an email account and then checking the correct behaviour of his class by retrieving the emails via POP3. This solution has some disadvantages:
- The tests would be very slow.
- The tests need special preconditions (context) to run (smtp server, email account).
- The tests would not only cover the functionality of the class alone but also the .NET framework class SmtpClient. Framework classes do not need to be tested, because it can be assumed that they work correctly. (Even if they do not, we can't do anything about that)
- The retrieval of emails via the POP3 protocol has to be implemented only for the automated tests, which are currently not need for the production system.
I would always prefer to use state verification, that means verifying the outcome of a method by inspecting the state of the manipulated object, but in this case the class has no state which can be verified. As we have seen, the state verification of a backdoor (a real email account) has many drawbacks.
An other solution would be the use of a mock object as a test double for the SmtpClient. Currently our SendToAll method is empty and uses no dependent object, which can be mocked out. My colleague decided to use mock objects to verify the behaviour of his class. He starts implementing an initial version of his method, before he finishes his test method. The initial version looks like this:
public class EmailSender
{
private SmtpClient smtpClient;
...
public EmailSender(
SmtpClient smtpClient,
string host,
int port,
string from,
params string[] recipients)
{
this.smtpClient = smtpClient;
this.from = from;
this.port = port;
this.host = host;
this.recipients = recipients;
}
public void SendToAll(string subject, string details)
{
smtpClient.Host = host;
smtpClient.Port = port;
foreach (string recipient in recipients)
{
smtpClient.Send(
from,
recipient,
subject,
details);
}
}
}
He already injected the SmtpClient object via the constructor and uses this object to send the emails. This is a nice try but unfortunately he can't mock out the SmtpClient yet. The SmtpClient framework class does not inherit from any interface nor does it declare its methods as virtual, so he can't create a mock object for SmtpClient. This lack of 'mockability' is quite common for most of the classes in the Microsoft .NET framework. Cwalina and Abrams [1] are describing rules for the design of framework classes: "Class-based APIs can be evolved with much greater ease than interface-based APIs because it is possible to add members to a class without breaking existing code". From the perspective of framework designers it is absolute understandable, that most of the classes do not use interfaces. As unit test authors we have to write our own interfaces which mirror the API of the framework classes as close as possible and later write very thin wrappers inheriting from our interface. My colleague only uses two properties and one method of the SmtpClient class. He only needs to declare these members in his new interface:
public interface ISmtpClient
{
string Host { get; set; }
int Port { get; set; }
void Send(string from, string recipients,
string subject, string body);
}
Now he can slightly modify his EmailSender class to use the ISmtpClient interface instead of SmtpClient framework class directly:
public class EmailSender
{
private ISmtpClient smtpClient;
...
public EmailSender(
ISmtpClient smtpClient,
string host,
int port,
string from,
params string[] recipients)
{
this.smtpClient = smtpClient;
...
}
...
}
He finishes his test and verifies the behaviour of the EmailSender class. He is using the NMock2 framework for his mock object:
using NMock2;
...
[Test]
public void SendToAll()
{
// Setup
Mockery mocks = new Mockery();
ISmtpClient smtpClientMock = mocks.NewMock<ISmtpClient>();
EmailSender sender = new EmailSender(
smtpClientMock,
"smtp.test.com",
25,
"sender@test.com",
"receiver1@test.com",
"receiver2@test.com");
Expect.Once.On(smtpClientMock).
SetProperty("Host").
To("smtp.test.com");
Expect.Once.On(smtpClientMock).
SetProperty("Port").To(25);
// We expect that the Send method is called for every recipient
Expect.Once.On(smtpClientMock).Method("Send").
With("sender@test.com", "receiver1@test.com",
"TestSubject", "TestDetails");
Expect.Once.On(smtpClientMock).Method("Send").
With("sender@test.com", "receiver2@test.com",
"TestSubject", "TestDetails");
// Exercise
sender.SendToAll("TestSubject", "TestDetails");
// Verify
mocks.VerifyAllExpectationsHaveBeenMet();
}
This test runs very fast and it does not need any special context like an smtp server or an email account. Before he can use the EmailSender class in a production environment he has to implement a thin wrapper around the SmtpClient framework class:
public class SmtpClientWrapper : ISmtpClient
{
private SmtpClient smtpClient = new SmtpClient();
public string Host
{
get { return smtpClient.Host; }
set { smtpClient.Host = value; }
}
public int Port
{
get { return smtpClient.Port; }
set { smtpClient.Port = value; }
}
public void Send(string from,
string recipients,
string subject,
string body)
{
smtpClient.Send(from, recipients, subject, body);
}
}
This wrapper needs not to be tested, because it adds no new functionality to the framework class. The EmailSender class can now be used in the background service:
public void Run()
{
EmailSender sender = new EmailSender(
new SmtpClientWrapper(),
"smtpserver.xy.com",
25,
"sender@xy.com",
"receiver1@xy.com",
"receiver2@xy.com");
while (!stopped)
{
// Check something
string checkResult = Check();
// Send email
sender.SendToAll("Check", checkResult);
// Sleep for 20 minutes
Thread.Sleep(20*60*1000);
}
}
Conclusion
Applying TDD as a new technique for software development is sometimes not easy for beginners. When we need to write software, which has to use framework classes, we need to use mock objects and we eventually have to introduce new interfaces and wrappers. The same problems we saw with SmtpClient arise when we have to use classes like System.ServiceProcess.ServiceController, System.Diagnostics.EventLog, System.DirectoryServices.ActiveDirectory.Domain, System.Net.NetworkInformation.NetworkInterface, System.Net.Sockets.Socket, System.Environment, Microsoft.Win32.Registry just to name a few. I hope this article can help a little bit to overcome the first hurdles when using TDD the first time.
References
[1] Cwalina, K., Abrams, B.: Framework Design Guidelines, Microsoft .NET Development Series, Addison Wesley, 2006