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.
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
No comments:
Post a Comment