I deal with sending emails a lot. For most of the tests they are pretty simple such as making sure the mail is formatted properly. In some cases I have to actually confirm that the mail client can deliver to a server and confirm that I can read it from that server. An End to End (E2E) test.
There's a lot of ways to approach this like bringing up a test mail server and POP3ing to it. Maybe use PaperCut and eyeball the results. What if there was a way your test could bring up an Smtp Server and you could use that?
First we need a simple mail client. Basically we are wrapping the SmtpClient so we can call it easier.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
namespace MailClient
{
public class MailClient
{
private readonly string _smtpHost;
private readonly int _smtpPort;
private readonly string _smtpUserName;
private readonly string _smtpPassword;
public MailClient(string smtpHost, int smtpPort, string smtpUserName, string smtpPassword)
{
if (smtpHost == null) throw new ArgumentException("smtpHost");
if (smtpUserName == null) throw new ArgumentException("smtpUserName");
if (smtpPassword == null) throw new ArgumentException("smtpPassword");
_smtpHost = smtpHost;
_smtpPort = smtpPort;
_smtpUserName = smtpUserName;
_smtpPassword = smtpPassword;
}
public void SendMail(MailMessage message)
{
SendMail(new[] { message });
}
public void SendMail(IEnumerable messages)
{
using (var client = new SmtpClient())
{
client.UseDefaultCredentials = true;
client.Host = _smtpHost;
client.Port = _smtpPort;
client.Credentials =
new NetworkCredential(_smtpUserName, _smtpPassword);
foreach (var message in messages)
{
client.Send(message);
}
}
}
public void SendMail(string from, string recipient, string subject, string body,
IEnumerable attachments)
{
var message = new MailMessage(from, recipient, subject, body);
if (attachments != null)
{
foreach (var a in attachments)
{
message.Attachments.Add(a);
}
}
SendMail(new[] { message });
}
}
}
With that out of the way we need a test. We are going to use two nuget packages in our test project.
With those references added we are going to need a couple classes. Most of these classes came from the tests from the SmtpServer project.
The FakeMessageStore will let us store messages in a list.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using SmtpServer;
using SmtpServer.Protocol;
using SmtpServer.Storage;
namespace MailClient.Tests
{
public class FakeMessageStore : MessageStore
{
public FakeMessageStore()
{
Messages = new List();
}
public override Task SaveAsync(ISessionContext context, IMessageTransaction transaction, CancellationToken cancellationToken)
{
Messages.Add(transaction);
return Task.FromResult(SmtpResponse.Ok);
}
public List Messages { get; }
}
}
SmtpServerDisposable will let us have a disposable SmtpServer for our tests.
using System;
namespace MailClient.Tests
{
public sealed class SmtpServerDisposable : IDisposable
{
readonly Action _delegate;
///
/// Constructor.
///
/// The SMTP server instance.
/// The delegate to execute upon disposal.
public SmtpServerDisposable(SmtpServer.SmtpServer server, Action @delegate)
{
Server = server;
_delegate = @delegate;
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
_delegate();
}
///
/// The SMTP server instance.
///
public SmtpServer.SmtpServer Server { get; }
}
}
TestMailServer will let us have a TestMailServer for our tests using the Disposable server. This is what the tests are going to interact with.
using System;
using System.Threading;
using SmtpServer;
namespace MailClient.Tests
{
public class TestMailServer
{
public FakeMessageStore MessageStore { get; }
///
/// The cancellation token source for the test.
///
public CancellationTokenSource CancellationTokenSource { get; }
public TestMailServer(FakeMessageStore store, CancellationTokenSource tokenSource)
{
MessageStore = store;
CancellationTokenSource = tokenSource;
}
public SmtpServerDisposable Start()
{
var options = new SmtpServerOptionsBuilder()
.ServerName("localhost")
.Port(25, 587)
.MessageStore(MessageStore)
.Build();
var server = new SmtpServer.SmtpServer(options);
var smtpServerTask = server.StartAsync(CancellationTokenSource.Token);
return new SmtpServerDisposable(server, () =>
{
CancellationTokenSource.Cancel();
try
{
smtpServerTask.Wait();
}
catch (AggregateException e)
{
e.Handle(exception => exception is OperationCanceledException);
}
});
}
}
}
So at this point we can now write the test. We want to send an email to our local test server then confirm we have the right subject, body and attachments.
using System;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MimeKit;
using SmtpServer.Mail;
namespace MailClient.Tests
{
[TestClass]
public class MailClientTests
{
[TestMethod]
public void CanSendMailWithAttachments()
{
var store = new FakeMessageStore();
var mailServer = new TestMailServer(store, new CancellationTokenSource());
using (var server = mailServer.Start())
{
var client = new MailClient("localhost", 25, "test", "test");
var subject = "This is some text in the subject";
var body = "This some text in the body";
var attachmentBody = "Sample File";
client.SendMail("test@test.com",
"test@test.com",
subject,
body,
new[] {new Attachment(new MemoryStream(Encoding.UTF8.GetBytes(attachmentBody)), "sample.txt")});
Assert.IsTrue(store.Messages.Any());
//Need MimeKit to decode the message so we can work with attachments.
var message = MimeMessage.Load(((ITextMessage) store.Messages.First().Message).Content);
Assert.IsTrue(message.Attachments.Count()==1);
foreach (var testAttachment in message.Attachments)
{
if (!(testAttachment is MimePart part)) continue;
using (var ms = new MemoryStream())
{
part.Content.DecodeTo(ms);
ms.Seek(0, SeekOrigin.Begin);
using (var sr = new StreamReader(ms))
{
Assert.AreEqual(sr.ReadToEnd(), attachmentBody);
}
}
}
Assert.AreEqual(message.TextBody, body);
Assert.AreEqual(message.Subject, subject);
}
}
}
}
Run it and the tests should pass. If this is the first time you ran it windows firewall may ask for permissions.
It is that easy to have a full E2E test with email transmission and confirming the reception and contents.