Testing Emails

Ever wished you could test the output of your MailMessages without actually sending the message anywhere? You could use the DeliveryMethod of the SmtpClient with SpecifiedPickupDirectory and try and round trip the email through the file system. This is troublesome as you have to remember to delete all the files in the output directory before calling Send, then scan the folder for the new file. It also prevents you from any sort of multi-threaded testing.

Rather than persist to the file system, I want to write a test like this:

[Test]
public void Email_is_formatted_correctly()
{
    var message = new MailMessage( 
                        "user@domain.com", 
                        "user@anotherdomain.com", 
                        "I'm sent in memory!", 
                        "Hello!" );
                        
    var messageText = GetMessageAsString( message, true );

    Expect( messageText, Contains( "Subject: I'm sent in memory!" ) );
    Expect( messageText, EndsWith( "Hello!" )  );
}

I can easily check that the message was formatted as I expected and the exact output sent across the wire. It looks something like this:

MIME-Version: 1.0
From: user@domain.com
To: user@anotherdomain.com
Date: 9 Aug 2009 12:23:10 -0700
Subject: I'm sent in memory!
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit

Hello!

The GetMessageAsString method relies on a private class MailWriter in the System.Net.Mail namespace. The writer simply writes the contents of a MailMessage to a stream that you pass in the constructor.

We create an instance of the writer and tell the message to send itself to the writer, then read the stream as a string:

private static string GetMessageAsString( MailMessage message, bool includeEnvelope )
{
    using( var stream = new MemoryStream() )
    {
        var writer = CreateWriter( stream );
        message.GetType()
            .InvokeMember( 
                "Send",
                BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
                null,
                message,
                new[] {writer, includeEnvelope} );

        stream.Seek( 0L, SeekOrigin.Begin );
        
        using( var reader = new StreamReader( stream ) )
            return reader.ReadToEnd();
    }
}

Creating the writer is a bit tricky since we can't reflect directly on internal types. Instead we have to search for them and invoke the constructor directly.

private static object CreateWriter( Stream stm )
{
    var systemAsm = typeof( MailMessage ).Assembly;
    Type[] types = null;
    try
    {
        types = systemAsm.GetTypes();
    }
    catch( ReflectionTypeLoadException ex )
    {
        types = ex.Types;
    }

    Type writerType = null;
    foreach( var type in types )
    {
        if( type.Name != "MailWriter" ) continue;

        writerType = type;
        break;
    }

    Debug.Assert( writerType != null );

    var constructor = writerType
        .GetConstructor( 
            BindingFlags.Instance | BindingFlags.NonPublic,
            null,
            new[] {typeof( Stream )},
            null );
                                    
    return constructor
        .Invoke( 
            BindingFlags.NonPublic, 
            null, 
            new object[] {stm}, 
            null );
}

For regular testing I'd move the CreateWriter to a static class where I can cache the writer Type and constructor.

Related Articles

Published : Jul 19, 2009
Views : 10256

Subscribe Subscribe | Blog Home

Downloads

Tags

  • test