How to Test Email Sending with GreenMail in Java Integration Tests
Email sending often looks simple from an application’s perspective: create a message, connect to SMTP, hand the message to a mail server, and continue. The difficult defects appear at the boundaries. A wrong port, malformed multipart content, missing reply-to address, or incorrect character encoding can all pass a unit test that mocks the mail client.
GreenMail provides an in-memory SMTP, SMTPS, POP3, IMAP, and API server for automated tests. It gives Java integration tests a real mail endpoint without sending messages to customers, relying on an external mailbox, or cleaning up test data in a shared account.
This makes it useful for Spring Boot services, notification workers, password-reset flows, and backend systems that publish email jobs asynchronously. A test can start GreenMail, configure the application to use its port, trigger the business operation, and inspect the complete message that was received.
For teams delivering software in Australia, this also helps keep testing independent of production mail providers and regional infrastructure. A developer in Sydney, a CI runner in Melbourne, and a staging environment in Brisbane can all verify the same SMTP behaviour without involving a real customer mailbox.
Why Mocking SMTP Is Often Insufficient
A mocked JavaMailSender can prove that a method was called with a particular object. It cannot prove that the message is serialised correctly, that SMTP authentication works, or that the generated MIME structure can be read by a mail client. Those failures commonly appear only after deployment.
An integration test with GreenMail exercises the application’s mail configuration and the SMTP protocol boundary. It can detect an invalid hostname, an unavailable port, a broken TLS setting, or an email that contains an unexpected content type. The test remains local and deterministic because GreenMail stores the received message in memory.
This boundary is especially valuable for transactional messages such as invoices, account confirmations, and delivery updates. An Australian retailer might process orders in AEST while a worker runs in UTC; inspecting the received message can expose incorrect date formatting before it reaches customers in Perth, Adelaide, or Darwin.
Adding GreenMail to a Java Test Suite
For Maven projects, add GreenMail as a test dependency. The exact version should follow the current release approved by the project, but the dependency typically has this shape:
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail-junit5</artifactId>
<version>${greenmail.version}</version>
<scope>test</scope>
</dependency>
The JUnit 5 module supplies extensions and lifecycle support. Teams using another test framework can create and stop a GreenMail instance directly. It is worth keeping the server inside the test source set so that production packaging never includes an embedded mail server.
The testing tutorials category includes related practical techniques for Java automation, integration boundaries, and repeatable test setup. GreenMail works particularly well alongside Spring Boot’s test context, Testcontainers, and Awaitility, although it does not need Docker.
| Testing approach | What it verifies | Main limitation |
|---|---|---|
| Mocked mail sender | Method calls and constructed arguments | Does not test SMTP or MIME delivery |
| GreenMail integration test | SMTP connection, message receipt, and content | Does not represent a real provider’s spam filtering |
| Mail provider sandbox | Provider API, authentication, and accepted requests | Can be slower and dependent on external services |
| Manual mailbox test | Visible rendering in a real client | Slow, fragile, and difficult to run in CI |
Starting GreenMail with JUnit 5
A test can start an SMTP server on a dynamically assigned port. Dynamic ports reduce collisions when tests run in parallel or when a local developer already has another mail service running.
import com.icegreen.greenmail.junit5.GreenMailExtension;
import com.icegreen.greenmail.util.ServerSetupTest;
import org.junit.jupiter.api.extension.RegisterExtension;
class NotificationIT {
@RegisterExtension
static GreenMailExtension greenMail =
new GreenMailExtension(ServerSetupTest.SMTP);
// tests go here
}
ServerSetupTest.SMTP uses GreenMail’s test configuration. For a Spring Boot application, override the mail properties in the test profile or with dynamic properties. The important values are the host, SMTP port, username, and password.
spring.mail.host=127.0.0.1
spring.mail.port=3025
spring.mail.username=test-user
spring.mail.password=test-password
spring.mail.properties.mail.smtp.auth=true
When the port is dynamic, obtain it from GreenMail rather than hard-coding 3025. With Spring tests, @DynamicPropertySource can register the live port before the application context creates its mail sender. This prevents a misleading test that starts GreenMail successfully while the application still points to another port.
Sending and Inspecting a Message
The test should trigger the same application service used by the production path. Avoid calling the mail library directly in the test, because that bypasses the configuration and business logic the integration test is intended to cover.
notificationService.sendWelcome(
"customer@example.test",
"Alex",
"https://example.test/activate"
);
assertTrue(greenMail.waitForIncomingEmail(5_000, 1));
MimeMessage message = greenMail.getReceivedMessages()[0];
assertEquals("customer@example.test",
message.getAllRecipients(Message.RecipientType.TO)[0].toString());
assertEquals("Welcome to Example", message.getSubject());
The .test top-level domain is reserved for examples, so it is safe for test data. GreenMail’s waitForIncomingEmail is preferable to an immediate assertion when the application sends mail through a worker thread. It waits for a bounded period and keeps a failed test understandable.
Further checks can cover the sender, reply-to address, custom headers, and message count. Assert that exactly one message arrives when one notification is expected. An assertion that merely checks “at least one” can allow duplicate delivery defects to pass unnoticed.
Verifying HTML, Text, and Attachments
Email content is usually multipart MIME rather than a simple string. A robust test should inspect the content type and walk through the body parts. For a message containing both plain text and HTML, verify the important user-visible details in each representation.
assertTrue(message.isMimeType("multipart/alternative"));
MimeMultipart multipart = (MimeMultipart) message.getContent();
String plainText = (String) multipart.getBodyPart(0).getContent();
String html = (String) multipart.getBodyPart(1).getContent();
assertTrue(plainText.contains("Activate your account"));
assertTrue(html.contains("href=\"https://example.test/activate\""));
The exact body-part order can vary depending on the mail framework. Production-quality test helpers should locate parts by MIME type instead of assuming that HTML is always index one. They can also decode quoted-printable or base64 content through the JavaMail APIs rather than comparing raw encoded text.
Attachments deserve their own assertions. Check the filename, disposition, content type, and byte content for an invoice or report. This catches a common regression where an attachment is included but has no filename, or where a PDF is accidentally labelled as text/plain.
Email tests should generally verify semantics rather than full HTML snapshots. Assert the subject, recipient, key links, branding text, and attachment metadata. Full markup comparisons become noisy when a template engine changes whitespace or adds harmless attributes.
Handling Asynchronous Mail Delivery
Many backend applications place an email command on a queue and return before the SMTP operation completes. A direct assertion immediately after the command will then be flaky. GreenMail can wait for receipt, but the application’s own transaction and worker must still be allowed to finish.
Use a bounded polling tool such as Awaitility when the application has multiple asynchronous stages:
await()
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> {
assertEquals(1, greenMail.getReceivedMessages().length);
});
A timeout should reflect the test environment, not an arbitrary long delay. Ten seconds may be reasonable for a local CI runner, while a heavily loaded shared runner may need a slightly larger bound. Keep the timeout finite so a broken worker does not stall the build.
Clear received messages before each test if the same GreenMail instance is reused. Better isolation comes from starting and stopping the server for each test class or suite according to execution time. Also reset queues, database fixtures, and idempotency keys; otherwise a previous test may make a duplicate-delivery assertion pass.
A Focused Verification Checklist
A small set of precise checks provides more value than asserting every header emitted by the mail library. Choose assertions that represent the contract between the application and its recipient.
- SMTP host and port come from test configuration
- Exactly one message arrives for one business event
- Recipient, sender, subject, and reply-to values are correct
- Plain-text and HTML alternatives contain expected content
For richer notification flows, include a second group of checks around data and operations:
- Template variables are escaped and rendered correctly
- Attachments have the expected name and media type
- A failed SMTP connection produces the intended retry behaviour
- Test messages and credentials never reach a real provider
These checks are useful in a continuous delivery pipeline because they fail close to the defect. They also make code review easier: a reviewer can see which user-visible and protocol-level guarantees the test protects.
Running GreenMail Reliably in CI
GreenMail is well suited to CI because it is local, fast, and does not require credentials for Gmail, Microsoft 365, or an Australian transactional email provider. Tests should bind to loopback, use reserved example addresses, and avoid depending on public DNS or an internet connection.
Separate integration tests from unit tests with a naming convention such as *IT, a Maven profile, or a JUnit tag. Run them on every pull request if they are quick enough, then repeat the full suite in the delivery pipeline. This gives developers early feedback while retaining a clear stage for broader verification.
Australian teams should also test timezone-sensitive templates deliberately. A customer in Sydney may receive an event scheduled according to daylight-saving rules, while Queensland does not observe daylight saving. Keep the clock injected and assert the intended zone instead of relying on the machine timezone. Similar care applies to Australian date formats, dollar amounts, and addresses used in invoices.
GreenMail cannot reproduce provider-side filtering, reputation scoring, throttling, or mailbox rendering differences. Those concerns belong in a small number of sandbox or staging checks. The integration suite should focus on the application’s contract with SMTP and the correctness of the generated message.
The practical next step is to add one GreenMail-backed test for a real notification path, using a dynamic SMTP port and assertions for recipient, subject, body, and delivery count.