How to test… Java Mail

Using the JavaMail-API is a quite convenient way to send and/or receive email... OK it feels a little old (and it is) but we can always use something like Apache Commons Mail's SimpleMail to give it a more elegant touch. In the end this is only "makeup" that relies on the API as well.

However easy using JavaMail-API is, writing tests for classes that use it is a challenge.

Consider this simple class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package de.assertagile.demonstration.howtotest.mail;
 
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
 
public class MailingClass {
 
    private final Properties mailProperties = new Properties();
    private final String imapHost;
    private final int imapPort;
 
    public MailingClass(String smtpHost, int smtpPort,
                        String imapHost, int imapPort,
                        String user, String password) {
        this.imapHost = imapHost;
        this.imapPort = imapPort;
        mailProperties.setProperty("mail.smtp.host", smtpHost);
        mailProperties.setProperty("mail.smtp.port", Integer.toString(smtpPort));
        mailProperties.setProperty("mail.user", user);
        mailProperties.setProperty("mail.password", password);
        mailProperties.setProperty("mail.store.protocol", "imap");
    }
 
    public void sendMail(String to, String from, String subject,
                         String content) throws MessagingException {
        Session session = Session.getDefaultInstance(mailProperties);
 
        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress(from));
        message.setRecipients(Message.RecipientType.TO, to);
        message.setSubject(subject);
        message.setText(content);
 
        Transport.send(message);
    }
 
    public Message[] receiveMail(String user, String password)
            throws MessagingException {
        Session session = Session.getDefaultInstance(mailProperties);
 
        Store store = session.getStore("imap");
        store.connect(imapHost, imapPort, user, password);
        Folder inbox = store.getFolder("INBOX");
 
        inbox.open(Folder.READ_ONLY);
 
        return inbox.getMessages();
    }
}

Let's write a test for it that does not require a mail server and will never spam sombody@example.com.

Solution A: Mock Mail using PowerMock

To test MailingClass we first need to mock Session.getDefaultInstance(mailProperties) and Transport.send(message) which are static methods... not very convenient but we could use something like PowerMock to do this anyway (see this StackOverflow).

This is a valid approach but I personally don't like it very much since it does not work well with Groovy tests.

Solution B: Wrap it

A generic solution would be wrapping everything that's hard to mock into one minimal class, mock it and accept that this wrapper will most probably never get a test.

Also a valid approach. If something goes wrong in the mailing process, the cause is easily found inside the wrapper class. However, it makes me change my class design and it leaves code untested.

Solution C: Use Mock-JavaMail

Mock-JavaMail (Maven) basically catches all mail that you send via JavaMail API. It requires very little configuration and effectively prevents your test from spamming sombody@example.com.

You just need to put it in your class path. Do not forget to scope/remove it or no mail will ever leave your application. Also, you should remove it if you decide for another solution or try to evaluate a GreenMail example for the next chapter of your blog post :-/.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package de.assertagile.demonstration.howtotest.mail
 
import org.jvnet.mock_javamail.Mailbox
import spock.lang.Specification
 
import javax.mail.Message
import javax.mail.internet.InternetAddress
 
class MailingClassMockJavaMailSpec extends Specification {
 
    String smtpHost = "smtp.somwehere.com"
    int smtpPort = 4712
    String imapHost = "imap.somwhere.com"
    int imapPort = 4711
    String user = "someUser"
    String password = "somePassword"
 
    MailingClass mailingClass = new MailingClass(smtpHost, smtpPort, imapHost, imapPort, user, password)
 
    def setup() {
        Mailbox.clearAll()
    }
 
    def "sending mails should work"() {
        given:
        String to = "somebody@somewhere.com"
        String from = "test@mailingclass.net"
        String subject = "Test"
        String content = "This content should be received after the call of sendMail."
 
        when:
        mailingClass.sendMail(to, from, subject, content)
 
        then:
        Mailbox.get(to).newMessageCount == 1
        Message message = Mailbox.get(to)[0]
        message.from == [new InternetAddress(from)]
        message.allRecipients == [new InternetAddress(to)]
        message.subject == subject
        message.content == content
    }
}
MailingClassMockJavaMailSpec.groovyview rawview file on GitHub

This is nice if we only send mails. However, MailingClass has a receiveMail method that is not covered here.

Solution D: Use GreenMail

GreenMail is a full mail server mock. It simulates SMTP(S), POP3(S), and IMAP(S) servers and supports mocking user accounts.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package de.assertagile.demonstration.howtotest.mail
 
import com.icegreen.greenmail.user.GreenMailUser
import com.icegreen.greenmail.util.GreenMail
import com.icegreen.greenmail.util.ServerSetupTest
import spock.lang.Specification
 
import javax.mail.Message
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
 
class MailingClassGreenMailSpec extends Specification {
 
    GreenMail greenMail = new GreenMail(ServerSetupTest.SMTP_IMAP)
    GreenMailUser greenMailUser = greenMail.setUser(
            "someuser@somewhere.com", "someUser", "somePassword")
 
    MailingClass mailingClass = new MailingClass(
            "localhost", ServerSetupTest.SMTP.port,
            "localhost", ServerSetupTest.IMAP.port,
            greenMailUser.login, greenMailUser.password)
 
    def setup() {
        greenMail.start()
    }
 
    def cleanup() {
        greenMail.stop()
    }
 
    def "sending mails should work"() {
        given:
        String to = "receiver@testmailingclass.net"
        String from = greenMailUser.email
        String subject = "Sending test"
        String content = "This content should be sent by the user."
 
        when:
        mailingClass.sendMail(to, from, subject, content)
 
        then:
        greenMail.receivedMessages.size() == 1
        Message message = greenMail.receivedMessages[0]
        message.from == [new InternetAddress(from)]
        message.allRecipients.contains(new InternetAddress(to))
        message.subject == subject
        message.content == "${content}\r\n"
    }
 
    def "receiving mails should work"() {
        given:
        String from = "sender@testmailingclass.net"
        String subject = "Sending test"
        String content = "This content should be received by the user."
        deliverMessage(from, subject, content)
 
        when:
        Message[] messages = mailingClass.receiveMail(
                greenMailUser.login, greenMailUser.password)
 
        then:
        messages.size() == 1
        Message message = messages[0]
        message.from == [new InternetAddress(from)]
        message.allRecipients == [new InternetAddress(greenMailUser.email)]
        message.subject == subject
        message.content == content
    }
 
    private MimeMessage deliverMessage(String from, String subject, String text) {
        MimeMessage message = new MimeMessage((Session) null)
        message.setFrom(new InternetAddress(from))
        message.setRecipients(Message.RecipientType.TO, greenMailUser.email)
        message.setSubject(subject)
        message.setText(text)
        greenMailUser.deliver(message)
        return message
    }
}
MailingClassGreenMailSpec.groovyview rawview file on GitHub

This tests sending and receiving of mails and gives us a 100% test coverage for our MailingClass.

The only downside of GreenMail that I can see is that it actually opens ports (ServerSetupTest.SMTP.port and ServerSetupTest.SMTP.port) on localhost which may be already in use (for example by another test running in parallel).

Notice that I had to add "\r\n" in the sending test's then block since the message that comes out of GreenMail went through a real SMTP protocol implementation adding that suffix. This may be confusing but it shows how GreenMail puts your test way closer to the real thing.

Solution E: Use javamail-mock2

javamail-mock2 is a rather new solution by salyh. It can be used in "fullmock" mode, which works just like solution C and "halfmock" which requires for your test classes to use "mock_*" protocols to work.

Example (Fullmock)

Making the protocol configurable or injecting the mocked transport is not a big deal for your own classes, it does not work for framework code. So I did not adjust my MailigClass but used javamail-mock2 in "fullmock" mode. Basically it has the same benefits and drawbacks as Mock-JavaMail, though the option of switching to "halfmock" is nice and it will support advanced protocol features like IMAP IDLE in the next release as salyh promised in his comment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package de.assertagile.demonstration.howtotest.mail
 
import de.saly.javamail.mock2.MockMailbox
import spock.lang.Specification
 
import javax.mail.Message
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
 
class MailingClassJavaMailMock2Spec extends Specification {
 
    MailingClass mailingClass = new MailingClass("smtpHost", 4711, "imapHost", 4712, "user", "password")
 
    def setup() {
        MockMailbox.resetAll()
    }
 
    def "sending mails should work"() {
        given:
        String to = "receiver@testmailingclass.net"
        String from = "test@mailingclass.net"
        String subject = "Test"
        String content = "This content should be received after the call of sendMail."
 
        when:
        mailingClass.sendMail(to, from, subject, content)
 
        then:
        MockMailbox.get(to).inbox.getByMsgNum(1).subject == subject
 
        MockMailbox.get(to).inbox.messageCount == 1
        Message message = MockMailbox.get(to).inbox.messages[0]
        message.from == [new InternetAddress(from)]
        message.allRecipients.contains(new InternetAddress(to))
        message.subject == subject
        message.content == "${content}"
    }
 
    def "receiving mails should work"() {
        given:
        String to = "receiver@testmailingclass.com"
        String from = "sender@testmailingclass.net"
        String subject = "Sending test"
        String content = "This content should be received by the user."
        deliverMessage(to, from, subject, content)
 
        when:
        Message[] messages = mailingClass.receiveMail(to, "")
 
        then:
        messages.size() == 1
        Message message = messages[0]
        message.from == [new InternetAddress(from)]
        message.allRecipients == [new InternetAddress(to)]
        message.subject == subject
        message.content == content
    }
 
    private MimeMessage deliverMessage(String to, String from, String subject, String text) {
        MimeMessage message = new MimeMessage((Session) null)
        message.setFrom(new InternetAddress(from))
        message.setRecipients(Message.RecipientType.TO, to)
        message.setSubject(subject)
        message.setText(text)
        MockMailbox.get(to).inbox.add(message)
        return message
    }
}
MailingClassJavaMailMock2Spec.groovyview rawview file on GitHub

Conclusion

I personally chose GreenMail as my favourite solution for testing mailing classes since it is works fine with Groovy tests and allows me to test the sending and receiving. However, the solution is not a pure class test since it starts and interacts with a minimal mail server and relies on the necessary ports not being in use. Mocking Session and Transport would be a much cleaner solution but it requires some dirty work and the resulting test would be of limited value compared to a GreenMail based one.

Mocking the Transport with javamail-mock2's "halfmock" mode seems to be much cleaner (at least you don't need to do the dirty work), but it does not work for stupid classes like my own MailigClass.

If you found another solution please let me know.

  • Greenmail does not provide full IMAP functionality though, their IDLE command support is missing :/

    • Sad to hear but good to know that. Have you found any better solution?

  • There’s another javamail mock library available (Imap IDLE will be supported in the next release) – JavaMail Mock2 https://github.com/salyh/javamail-mock2

    • I will have a look at it. Thanks for the hint.

    • I just added an example using JavaMail Mock2. I still like GreenMail a little better, but “halfmock” mode seems to be worth tying (though it does not work for my example class here.

  • javamail-mock2 now has (as promised) IMAP IDLE support, see https://github.com/salyh/javamail-mock2