Factory patterned to be unit tested

Factory methods are used in test-driven development to allow classes to be put under test.

For example you have below snippet of code to create and send the email:

public class EmailSender implements EmailService {

    //Compose email
    public static Message createMessage(MessageType.MimeMessage, String toAddresses, String subject ...) throws EmailException {
        Message msg;

        if(messageType == MimeMessage){
            try{
                msg = createMimeMessage();
            } catch(Exception exception){
                throw new EmailException("Failed to create MIME message", messagingException);
            }
        } else {
            throw new UnsupportedOperationException("Message type not supported");
        }

        return msg;
    }
    
    
    @Override
    public void send(String to, String subject, String body, String from) throws EmailException {
        MimeMessage message = createMessage(MessageType.MimeMessage, toAddresses, subject ...);
        if(messageTooBig(message))
            throw new UnsupportedOperationException("Message too big");
        mailSender.send(message);
     }
}

1. This class needs to be unit tested, but you can't really send the actual emails around, otherwise, when running in teamcity, everytime it runs there will be
   emails flying around. So how can we get it tested ?
   
   In general as we all know we need to mock/spy(mockito) "mailSender", then we can control how it behaves when sending out the test message we compose in test class.
   But in terms of the controlled behavior, we have two options:
   
   1) Make mailSender.send do nothing, we just count the number of times it would be invoked, e.g. when the message is too big, the method should not be called at all.

    @Test
    public void test_EqualToMaxSize() throws EmailException{
        String subject = "";
        String body = getTestMessageBody();
        //getMimeMessage(), this logic is the same as createMessage above.
        MimeMessage msg = getMimeMessage(TESTMAILADDRESS, TESTMAILADDRESS, body, subject);
        when(emailService.getMimeMessage(TESTMAILADDRESS,  INVALIDADDRESS, body, subject)).thenReturn(msg);
        doNothing().when(mailSender).send(msg);

        emailService.sendHtml(TESTMAILADDRESS, subject, body, TESTMAILADDRESS);
        verify(mailSender, times(1)).send(msg);
    }

    
    @Test
    public void test_MoreThanMaxSize() throws EmailException {
        String subject = "";
        String body = getTestMessageBody();

        try{
            emailService.sendHtml(TESTMAILADDRESS, subject, body, TESTMAILADDRESS);
            fail("expected email exception when email size is more than max size");
        } catch(EmailException emailException){
            assertEquals("Email breaches the threshold of " + MAXSIZE + "M when sending email with subject: test_MorethanMaxSize", emailException.getMessage());
        }
    }

   2) Register an "answer" callback to the method, so everytime it is called we track down what's the email subject and how it sends to, then compare it with the expected values.
      normally we can have a Hashmap to hold all these data in our test class.
      
     private void mockSomething(String fromAddress, String toAddress, String subject, String body, String delimiters) throws EmailException {
        MimeMessage msg = (MimeMessage) EmailMessageFactory.createMessage(mailSender, EmailMessageFactory.MessageType.MimeMessage, toAddress, subject, body, fromAddress);
        when(EmailSender.createMessage(mailSender, EmailMessageFactory.MessageType.MimeMessage, toAddress, subject, body, fromAddress)).thenReturn(msg);

        doAnswer(new Answer<Object>() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Message message = (Message) invocation.getArguments()[0];
                for (Address recipientAddress : message.getRecipients(Message.RecipientType.TO)) {
                    if (recipientAddress instanceof InternetAddress) {
                        //whoReceivedEmails is defined in the test class to hold the actual test result.
                        whoReceivedEmails.put(((InternetAddress) recipientAddress).getAddress(), message.getSubject());
                    }
                }

                return null;
            }
        }).when(mailSender).send(msg);
    }

    @Test
    public void test_send_EqualToMaxSize() throws EmailException{

        String fromAddress = "test@test.com";
        String toAddress = "test@test.com";

        String subject = "test_sendHtml_WithContentEqualToMaxSize";
        String body = getTestMessageBody(MAX_SIZE);
        mockMessageCreateAndSendProcess(fromAddress, toAddress, subject, body, DELIMITERS);

        emailService.sendHtml(toAddress, subject, body, fromAddress);
        assertEquals(whoReceivedEmails.size(), 1);
        assert(whoReceivedEmails.keySet().contains("test@test.com"));
    }

    @Test
    public void test_send_MoreThanMaxSize() throws EmailException {

        String fromAddress = "test";
        String toAddress = "test";

        String subject = "test";
        String body = getTestMessageBody(1.1 * MAX_SIZE);

        try{
            emailService.sendHtml(toAddress, subject, body, fromAddress);
            fail("expected email exception when email size is more than max size");
        } catch(EmailException emailException){
            assertEquals("Email breaches the threshold of " + MAX_SIZE + "M when sending email with subject: test_sendHtml_WithContentMoreThanMaxSize", emailException.getMessage());
        }
    }
      

As you can see, you need to call EmailSender.createMessage() to make sure it returns the message we composed in the test, but this reveals another issue, "createMessage" is not supposed to be public,
otherwise it breaks the rule of encapsulation, we should try to limit the access to these members.

What's the solution ?

This is the point where factory pattern get in, we move the "createMessage' our of "EmailSender" and put it to a separate factory class as below:


public class EmailMessageFactory {

    private static final String DELIMITER = ";";

    public static Message createMessage(MessageType messageType, String to, String subject ...) throws EmailException {
        Message resultMessage;

        if(messageType == MimeMessage){
            try{
                resultMessage = mailSender.createMimeMessage();
            } catch(AddressException addressException){
                throw new EmailException("Failed to create MIME message due to invalid address", addressException);
            } catch(MessagingException messagingException){
                throw new EmailException("Failed to create MIME message", messagingException);
            }
        } else {
            throw new UnsupportedOperationException("Message type not supported");
        }

        return resultMessage;
    }
}


This way we don't need to care about the creation of messages anymore, and if other parallel class/objects would like to create some other type of messages, they just need to call this method with different parameters then.

And for the unit tests, we can get this separate factory class tested properly, for "EmailSender", we need to update above "mockMessageCreateAndSendProcess" to below:

     private void mockSomething(String fromAddress, String toAddress, String subject, String body, String delimiters) throws EmailException {
        MimeMessage msg = (MimeMessage) EmailMessageFactory.createMessage(mailSender, EmailMessageFactory.MessageType.MimeMessage, toAddress, subject, body, fromAddress);
        when(EmailMessageFactory.createMessage(mailSender, EmailMessageFactory.MessageType.MimeMessage, toAddress, subject, body, fromAddress)).thenReturn(msg);

        doAnswer(new Answer<Object>() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Message message = (Message) invocation.getArguments()[0];
                for (Address recipientAddress : message.getRecipients(Message.RecipientType.TO)) {
                    if (recipientAddress instanceof InternetAddress) {
                        whoReceivedEmails.put(((InternetAddress) recipientAddress).getAddress(), message.getSubject());
                    }
                }

                return null;
            }
        }).when(mailSender).send(msg);
    }

    
This way, EmailSender will purely focus on its own job to send out emails while the factory will concentrate on creation of messages, this is separation of roles and decoupling.

posted @ 2015-08-18 21:09  glf2046  阅读(85)  评论(0编辑  收藏  举报