简体   繁体   中英

How to write my first Unit-test for Email-function

This is just a smaller part of an email sending application, but I'm trying to implement unit-testing for the application. And I've read up a bit on unit testing so far, and I understand you should only test single functions instead of entire classes. How do I test this code?

private void Send(IEnumerable<Customer> customers, string body)
        {
            string titleOfEmail = "Welcome as a new customer at Company!";
            string ourEmailAddress = "info@company.com";
            int NumberOfRetriesOnError = 2;
            int DelayOnError = 10;

            foreach (var customer in customers)
            {


              for (int i = 0; i <= NumberOfRetriesOnError; ++i)
                {
                    try
                    {
                        Sender.Send(ourEmailAddress, customer.Email, titleOfEmail);
                        return;
                    }
                    catch (SmtpException e)
                    {
                        if (i < NumberOfRetriesOnError)
                            Thread.Sleep((i + 1) * DelayOnError);
                        else
                            Errors.Add(e.Message + customer); // Include customeremail
                    }
                }
}

Edit: Rest of the information probably not needed for the rest

public interface IMailSender
{
    void Send(string from, string to, string body);
}
sealed class NullMailSender : IMailSender
{
    void IMailSender.Send(string from, string to, string body)
    {

    }
}

sealed class SmtpMailSender : IMailSender
{
    void IMailSender.Send(string from, string to, string body)
    {
        System.Net.Mail.MailMessage mail = new System.Net.Mail.MailMessage();
        mail.From = new System.Net.Mail.MailAddress(from);
        System.Net.Mail.SmtpClient smtp = new System.Net.Mail.SmtpClient("yoursmtphost");
        mail.To.Add(to);
        mail.Body = body;
        smtp.Send(mail);

    }
}     

And this part is the rest of the top function, the whole class

public class SendingMail
{
    public List<string> Errors { get; } = new List<string>();

    public IEnumerable<Customer> Customers { get; set; }

    public IEnumerable<Order> Orders { get; set; }

    public IMailSender Sender { get; set; }

    public void SendWelcomeEmails()
    {
        var template = Resources.WelcomeEmailTemplate;
        Send(GetNewCustomers(), Resources.WelcomeEmailTemplate);
    }

    public void SendComeBackEmail()
    {
        var template = Resources.WelcomeEmailTemplate;
        var emailContent = String.Format(template);
        Send(GetCustomersWithoutRecentOrders(), Resources.ComeBackEmailTemplate);
    }

    private IEnumerable<Customer> GetNewCustomers()
    {
        var yesterday = DateTime.Now.Date.AddDays(-1);
        return Customers.Where(x => x.CreatedDateTime >= yesterday);
    }
    private IEnumerable<Customer> GetCustomersWithoutRecentOrders()
    {
        var oneMonthAgo = DateTime.Now.Date.AddMonths(-1);

        return Customers.Where(c => {
            var latestOrder = Orders
                .Where(o => o.CustomerEmail == c.Email)
                .OrderByDescending(o => o.OrderDatetime)
                .FirstOrDefault();

            return latestOrder != null
                && latestOrder.OrderDatetime < oneMonthAgo;
        });
    }

Okay, I think what might be tripping you up is that you haven't broken out your function according to SRP (Single Responsibility Principle.)

Put it this way: what all is your function currently responsible for?

  1. Setting up the Title & From-Address of the email
  2. Looping Through Each Customer
  3. Calling Sender
  4. Handling Errors

... so right off the bat, your unit tests would have to try to handle all those separate things in calls to the function. Unit Tests hate that.

But what if you wrote your function differently?

// needs System.Net.Mail for its MailMessage class - but you could write your own
private void Send(IEnumerable<Customer> customers, string body)
{
    foreach(Customer customer in customers)
        Send(customer, body);
}
private void Send(Customer customer, string body)
{
    MailMessage message = GenerateMessage(customer, body);
    // your code for sending/retrying; omitted to keep the code short
}
private MailMessage GenerateMessage(Customer customer, string body)
{
    string subject = "...";
    string from = "...";
    string to = customer.Email;
    MailMessage retVal = new MailMessage(from, to, subject, body);
    return retVal;
}

Okay, take a look at the Unit Testing picture now. Suddenly, you can test the generation of an email message without sending an email. Just create a dummy customer, pass it into the GenerateMessage function, and validate the results it passes back (no email even gets sent.)

As for the emailing itself? That's a bit tougher. You've got two options. Option #1 is to just generate a dummy customer with your personal/group's email address, and have it actually go ahead and send an email to you. Not ideal, but it'll make sure the email code works. But another option is to modify the Sender class (or wrap it, if you don't control it) - something like:

interface ISender
{
    void Send(/* the args you've got for your send function */);
}
class Sender : ISender
{
    void Send(/* args */) { /* code */ }
}
class DummySender : ISender
{
    void Send(/* args */)
    {
        // code to validate the unit tests of Send() are passing in correctly (and you won't have it actually send anything.)
    }
}

... and then, instead of calling 'Sender.Send' directly, you pass in the ISender you're planning on using to perform the send. The regular code would pass in an instance of Sender; your unit tests would pass in an instance of DummySender.

EDIT (to help with new info given)

That part with IMailSender? That's actually perfect.

First up, instead of just hooking right into 'Sender', you instead pass a IMailSender object to your Send() functions as an additional argument. Then you can write something like:

public class UnitTestDummySender : IMailSender
{
    public string fromAddressIShouldGet;
    public string toAddressIShouldGet;
    public string bodyIShouldGet;
    public void Send(string from, string to, string body)
    {
        // check to make sure the arguments passed in match those properties
    }
}

See how this works? Instead of just calling Send(), you do something like this:

// your regular code:
Send(custList, body, myNormalSenderClass);

// your unit test code:
UnitTestDummySender faker = new UnitTestDummySender();
// lines to set the faker properties to be what the email should work out to be
Send(myFakeCustList, body, faker);

Hope that helps! :-)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM