Kash Farooq's software development blog

.NET Developer

More TDD with wrapper classes

Posted by Kash Farooq on June 23, 2012

I have previously blogged about using wrapper classes to test the hard to test. In that post I made things easy for myself by using the parameterless constructor of the SmtpClient class.

I thought I’d revisit this area of testing, and try something different. This time I’ve used Moq rather than Rhino Mocks for starters. But I’m also going to pretend that SmtpClient does not have a parameterless constructor.  If a class you need to make testable does not have a parameterless constructor, it makes things a bit trickier:

public class SystemUnderTest
  public void MethodToBeTested()
    var mailMessage = new MailMessage { Subject = "Hello", From = new MailAddress("Me@Me.com") };
    mailMessage.To.Add(new MailAddress("You@You.com"));
    var smtpClient = new SmtpClient("smtp.kashfarooq.com", 25);

A number of things in the above code need to be addressed and tested. The SMTP client is being instantiated in the method with the host and port being sent in via a the constructor, which makes things awkward to test. Apart from that complication, I’d like to make sure the correct host and port are being used. And I’d also like to make sure the correct details are being set in the MailMessage object.

First, we need an interface so we can mock the Send method. This matches the SmtpClient.Send method exactly:

public interface ISmtpClient
  void Send(MailMessage mailMessage);

We also need a wrapper class that will wrap SmtpClient and the constructor we are planning to use:

public class SmtpClientWrapper : SmtpClient, ISmtpClient
  public SmtpClientWrapper(string host, int port) : base(host, port)  { }

The above wrapper class inherits the interface defined above and the class we are trying to make testable: SmtpClient. And as it derives from SmtpClient, it automatically implements ISmtpClient.Send(MailMessage).

Next, we’ll create a suitable constructor for our SystemUnderTest class so that we can inject in some mocks. Because the MethodToBeTested creates the SmtpClient object, I’m going to use the “Using a Func instead of a Factory” trick:

private readonly Func<string, int, ISmtpClient> createSmtpClient;

public SystemUnderTest(Func<string, int, ISmtpClient> createSmtpClient)
  this.createSmtpClient = createSmtpClient;

public SystemUnderTest() : this((host,port) => new SmtpClientWrapper(host, port)) {}

So, what’s going on here? I have created a Func that will take a string and int (the host and port) and return an ISmtpClient. From my test I’ll use the first constructor. The production code will use the second constructor. In both cases, the Func supplied is stored in createSmtpClient – and this will be called from MethodToBeTested to actually create the SmtpClient.

I’m now in full control of all the dependencies. I can catch the MailMessage parameter sent to the Send method, and the host and port name sent to the createSmtpClient Func, which ultimately gets sent to to the SmtpClient(string host, int port) constructor.

And here is the test that creates a mock ISmtpClient and a Func that returns it, and also catches the parameters sent to it so the appropriate asserts can be done:

public void CorrectEmailSentViaTheCorrectSmtpHost()
  string actualHostName=string.Empty;
  int actualPort=0;
  MailMessage actualMailMessage = null;

  var mockSmtpClient = new Mock<ISmtpClient>();
     .Setup(x => x.Send(It.IsAny<MailMessage>()))
     .Callback((MailMessage message) => actualMailMessage = message); //Catch the MailMessage sent to the Send method

  //Func to return an ISmtpClient and catch data sent to it
  Func<string, int, ISmtpClient> createSmtpClient = (host, port) =>
         actualHostName = host; //catch the hostname used
         actualPort = port; //catch the port used
         return mockSmtpClient.Object; //return mocked SmtpClient

  //Call system under test
  var sut = new SystemUnderTest(createSmtpClient);


[Incidentally, I’m using the excellent FluentAssertions in my asserts.]

Sorry, the comment form is closed at this time.

%d bloggers like this: