Sending Emails is the most common task for most backend applications. Java provides the Java Mail API - a platform and protocol-independent framework to build mail and messaging applications.
You can find documentation here.
In this tutorial, you will get simple steps to set up JavaMail in your Java Project + implement JavaMail API to build and send emails on SMTP protocol and use the tool GreenMail Extension and Testcontainers to test that emails are gone in a Mail Inbox.
Dependencies
Here are the dependencies used in the pom.xml
to make this tutorial work.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail-junit5</artifactId>
<version>1.6.11</version>
<scope>test</scope>
</dependency>
Gmail SMTP Minimal Configuration
To be able to send an email, you have to configure an SMTP server. For this demo, I use the basic Gmail SMTP Server configuration in the application.yaml file.
spring:
mail:
password: aSimplePAssword
username: yourmail@gmail.com
host: smtp.gmail.com
port: 587
protocol: smtp
properties:
mail:
smtp:
auth: true
starttls:
enable: true
Mail Service
First, we must define a simple email service within our application. Normally, one could add additional options, such as sending images or PDFs (Using a MimeMessage from JavaMailSender), but this tutorial is focused on testing the integration of email sending.
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender mailSender;
public void notifyUser(String email, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("test.sender@hotmail.com");
message.setSubject("Message from Java Mail Sender");
message.setText(content);
message.setTo(email);
mailSender.send(message);
}
}
Mail Controller
Now letβs define our controller, where we can expose the URL that we will use to dispatch email.
@RestController
@RequiredArgsConstructor
@RequestMapping("/notify")
public class EmailController {
private final EmailService emailService;
@PostMapping("/user")
public void createEmailNotification(@Valid @RequestBody EmailRequest request) {
emailService.notifyUser(request.getEmail(), request.getContent());
}
}
To dispatch the email from one service to one another, we can have to use an EmailRequest
object.
@Getter
@Setter
public class EmailRequest {
@Email
private String email;
@NotBlank
private String content;
}
Integration Testing
Once everything is okay in the production code, we should write an integration test to ensure that our code is perfectly sending the emails. Then, while registering the GreenMail extension, we can configure the email server and decide which protocols we need for testing.
@RegisterExtension
static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
.withConfiguration(GreenMailConfiguration.aConfig().withUser("user", "admin"))
.withPerMethodLifecycle(false);
We can use this information to override the configuration of our application by placing an application.yml
inside src/test/resources with the following content.
spring:
mail:
username: user
password: admin
host: 127.0.0.1
port: 3025
protocol: smtp
The default behaviour of this extension is to start and stop the GreenMail server for each test method. This will give us an empty email sandbox server for each test execution.
We can override this default behaviour using .withPerMethodLifecycle(false)
to share a GreenMail server for all test methods of a test class.
When using this approach, however, we must ensure that our tests are independent and don't fail if a previous test sends an email to the same inbox. This can be mitigated by using random email addresses.
We use @SpringBootTest
for our integration test to start the whole Spring Context. To invoke our endpoint, we use the TestRestTemplate
that is autoconfigured for us as we also start the embedded Tomcat βwebEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT
Putting it all together, a basic integration test for verifying the email transport looks like the following:
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class EmailControllerTest {
@RegisterExtension
static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
.withConfiguration(GreenMailConfiguration.aConfig().withUser("user", "admin"))
.withPerMethodLifecycle(false);
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void should_send_email_to_user_with_green_mail_extension() throws JSONException {
// Arrange
JSONObject emailJsonObject = new JSONObject();
emailJsonObject.put("email", "tester@spring.com");
emailJsonObject.put("content", "Hello this is a simple email message");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> emailRequest = new HttpEntity<>(emailJsonObject.toString(), headers);
// Act
ResponseEntity<Void> response = testRestTemplate.postForEntity("/notify/user", emailRequest, Void.class);
// Assert
Assertions.assertEquals(200, response.getStatusCodeValue());
MimeMessage receivedMessage = greenMail.getReceivedMessages()[0];
Assertions.assertEquals(1, receivedMessage.getAllRecipients().length);
Assertions.assertEquals("tester@spring.com", receivedMessage.getAllRecipients()[0].toString());
Assertions.assertEquals("test.sender@hotmail.com", receivedMessage.getFrom()[0].toString());
Assertions.assertEquals("Message from Java Mail Sender", receivedMessage.getSubject());
Assertions.assertEquals("Hello this is a simple email message", GreenMailUtil.getBody(receivedMessage));
}
}
After invoking our endpoint, we can get all captured emails from the GreenMail extension.
But Wait, π€!
Emails are asynchronous tasks, so to ensure that we have this operation performing well, we can use a tool called Awaitability.
To do so, we will first add another dependency in our pom.xml.
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
Once we have added the dependency in our project, now we can modify our test code to verify that after 2 seconds, we receive an email.
Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(()->{
MimeMessage receivedMessage = greenMail.getReceivedMessages()[0];
assertEquals(1, receivedMessage.getAllRecipients().length);
assertEquals("tester@spring.com", receivedMessage.getAllRecipients()[0].toString());
assertEquals("test.sender@hotmail.com", receivedMessage.getFrom()[0].toString());
assertEquals("Message from Java Mail Sender", receivedMessage.getSubject());
assertEquals("Hello this is a simple email message", GreenMailUtil.getBody(receivedMessage));
});
Integration Testing with Testcontainers
We can also test our EmailService using GreenMail Testcontainers. To do so, we can use his Docker Image to start a local GreenMail Docker container.
First, we will add the JUnit Jupiter extension from Testcontainers to manage the container lifecycle.
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.6</version>
<scope>test</scope>
</dependency>
New you can define a GenericContainer using the official GreenMail Docker Image. You can use the environment variable GREENMAIL_OPTS
to map our settings in application.properties.
Next, we have to tell Testcontainers which port to expose and which log message signals that the container is ready to receive traffic:
@Container
static GenericContainer greenMailGenericContainer = new GenericContainer<>(DockerImageName.parse("greenmail/standalone:latest"))
.waitingFor(Wait.forLogMessage(".*Starting GreenMail standalone.*", 1))
.withEnv("GREENMAIL_OPTS", "-Dgreenmail.setup.test.smtp -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.users=user:admin")
.withExposedPorts(3025);
As Testcontainers will map GreenMail's 3025 port to a random and ephemeral port on our machine, the address is dynamic. Using @DynamicPropertySource
we can override the dynamic parts of our email server configuration before starting the Spring Test Context.
@DynamicPropertySource
static void configureMailHost(DynamicPropertyRegistry registry) {
registry.add("spring.mail.host", greenMailGenericContainer::getHost);
registry.add("spring.mail.port", greenMailGenericContainer::getFirstMappedPort);
}
Now, here is how the complete test class will look like.
@Testcontainers
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EmailControllerContainerTest {
@Container
static GenericContainer greenMailGenericContainer = new GenericContainer<>(DockerImageName.parse("greenmail/standalone:latest"))
.waitingFor(Wait.forLogMessage(".*Starting GreenMail standalone.*", 1))
.withEnv("GREENMAIL_OPTS", "-Dgreenmail.setup.test.smtp -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.users=user:admin")
.withExposedPorts(3025);
@DynamicPropertySource
static void configureMailHost(DynamicPropertyRegistry registry) {
registry.add("spring.mail.host", greenMailGenericContainer::getHost);
registry.add("spring.mail.port", greenMailGenericContainer::getFirstMappedPort);
}
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void should_send_email_to_user_with_correct_payload() throws JSONException {
// ARRANGE
JSONObject emailJsonObject = new JSONObject();
emailJsonObject.put("email", "tester@spring.com");
emailJsonObject.put("content", "Hello this is a simple email message");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> emailRequest = new HttpEntity<>(emailJsonObject.toString(), headers);
// ACT
ResponseEntity<Void> response = testRestTemplate.postForEntity("/notify/user", emailRequest, Void.class);
// ASSERT
Assertions.assertEquals(200, response.getStatusCodeValue());
}
}
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn β’ Twitter β’ GitHub or Medium
This setup (Integration Tests with Testcontainers) might be useful if you want to share one GreenMail instance for multiple test classes and just need a running email server for your context to start. You can also use this standalone GreenMail Docker image during local development.
Now we can run the tests and see it passed.
Wrap up
In this article, you have seen how to :
- Send email using JavaMailSender
- Create Integration Test for Email Sending
- Test Asynchronous Operations (like sending emails)
- Create Integration Tests using Testcontainers
Hope this tutorial was helpful, you can found the entire code for this tutorial on GitHub.