39

Use GreenMail For Spring Mail (JavaMailSender) JUnit 5 Integration Tests

 3 years ago
source link: https://rieckpil.de/use-greenmail-for-spring-mail-javamailsender-junit-5-integration-tests/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Use GreenMail For Spring Mail (JavaMailSender) JUnit 5 Integration Tests

December 22, 2020

First time here? Get an overview of all topics you'll find answers for on this blog here.

Sending emails is a common responsibility of enterprise applications. With the Spring Boot Starter Mail dependency, this becomes a trivial task. But how can we write integration tests to verify our functionality without sending actual emails to a user? With this blog post, we'll introduce GreenMail to write integration tests with JUnit 5 for sending emails with a Spring Boot application.

Introduction to the Spring Boot project

The project for demonstrating how to write integration tests for sending emails with the JavaMailSender is straightforward. We're including the following Spring Boot Starters and are using Java 11:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>de.rieckpil.blog</groupId>
  <artifactId>spring-boot-test-mail-sending</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-boot-test-mail-sending</name>
  <properties>
    <java.version>11</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Further test dependencies -->
  </dependencies>
  <build>
    <!-- default Spring Boot Maven and Failsafe Plugin -->
  </build>
</project>

Our application has one responsibility. Whenever we perform an HTTP POST to /notifications with valid payload, we'll notify a user by sending him/her an email:

@RestController
@RequestMapping("/notifications")
public class NotificationController {
  private final NotificationService notificationService;
  public NotificationController(NotificationService notificationService) {
    this.notificationService = notificationService;
  @PostMapping
  public void createNotification(@Valid @RequestBody NotificationRequest request) {
    this.notificationService.notifyUser(request.getEmail(), request.getContent());

We're using Bean Validation to ensure our clients pass a valid email address and non-empty email messages:

public class NotificationRequest {
  @Email
  private String email;
  @NotBlank
  private String content;
  // getters & setters

The actual email transport happens inside the NotificationService that uses the JavaMailSender from Spring that is auto-configured for us.

Sending emails with JavaMailSender from Spring

With Spring Boot's auto-configuration mechanism we get a ready-to-use JavaMailsender bean whenever we specify the spring.mail.* properties.

You can inspect the Spring Boot classes MailSenderAutoConfiguration and MailSenderPropertiesConfiguration if you want to understand how and when this auto-configuration happens.

A minimal configuration for using Google's Gmail SMTP server looks like the following:

spring:
  mail:
    password: t0pS3cReT
    username: [email protected]
    host: smtp.gmail.com
    port: 587
    protocol: smtp
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true

We can then inject the JavaMailSender and start sending emails from our Spring Boot application:

@Service
public class NotificationService {
  private final JavaMailSender javaMailSender;
  public NotificationService(JavaMailSender javaMailSender) {
    this.javaMailSender = javaMailSender;
  public void notifyUser(String email, String content) {
    SimpleMailMessage mail = new SimpleMailMessage();
    mail.setFrom("[email protected]");
    mail.setSubject("A new message for you");
    mail.setText(content);
    mail.setTo(email);
    this.javaMailSender.send(mail);

For sending more advanced emails (e.g. with an attachment or HTML payload) we can create a MimeMessage using the JavaMailSender. However, for our testing demo the SimpleMailMessage is perfectly fine.

Now, when writing integration tests for this component of our application, we don't want to connect to a real SMTP server and deliver emails. Otherwise, our users would receive funny test messages whenever we run our test suite.

A better approach is to use a local sandbox email server that allows capturing and verifying all email traffic. GreenMail is such an email server that allows testing both sending and receiving mails. It's open-source, written in Java and we can easily integrate it into our project.

Writing integration tests with GreenMail and JUnit 5

There are multiple ways to integrate GreenMail for our integration test. Let's start with the most intuitive approach and register GreenMail's JUnit Jupiter extension.

We'll need the following GreenMail dependency for this:

<dependency>
  <groupId>com.icegreen</groupId>
  <artifactId>greenmail-junit5</artifactId>
  <version>1.6.1</version>
  <scope>test</scope>
</dependency>

While registering the GreenMail extension, we can configure the email server and decide which protocols we need for testing. As our demo application only sends emails, activating SMTP is enough:

@RegisterExtension
static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
  .withConfiguration(GreenMailConfiguration.aConfig().withUser("duke", "springboot"))
  .withPerMethodLifecycle(false);

As part of setting up the GreenMail server, we create a service user. We can use this information to override the configuration of our application by placing a application.yml inside src/test/resources with the following content:

spring:
  mail:
    password: springboot
    username: duke
    host: 127.0.0.1
    port: 3025 # default protocol port + 3000 as offset
    protocol: smtp
    test-connection: true

The default behavior of this extension is to start and stop the GreenMail server for each test method. With .withPerMethodLifecycle(false) we'll start one GreenMail instance as part of JUnit Jupiter's beforeAll lifecycle callback for all our test methods.

We use @SpringBootTest for our integration test to start the whole Spring Context. To invoke our endpoint we use the TestRestTemplate that is auto-configured 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:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class NotificationControllerIT {
  @RegisterExtension
  static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
    .withConfiguration(GreenMailConfiguration.aConfig().withUser("duke", "springboot"))
    .withPerMethodLifecycle(false);
  @Autowired
  private TestRestTemplate testRestTemplate;
  @Test
  void shouldSendEmailWithCorrectPayloadToUser() throws Exception {
    String payload = "{ \"email\": \"[email protected]\", \"content\": \"Hello World!\"}";
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<String> request = new HttpEntity<>(payload, headers);
    ResponseEntity<Void> response = this.testRestTemplate.postForEntity("/notifications", request, Void.class);
    assertEquals(200, response.getStatusCodeValue());
    MimeMessage receivedMessage = greenMail.getReceivedMessages()[0];
    assertEquals("Hello World!", GreenMailUtil.getBody(receivedMessage));
    assertEquals(1, receivedMessage.getAllRecipients().length);
    assertEquals("[email protected]", receivedMessage.getAllRecipients()[0].toString());

After invoking our endpoint we can request all captured emails from the GreenMail extension.

The test assertion above is quite naive as we expect the email to be delivered right after invoking our endpoint. We can also wrap our expectations with Awaitility to better verify this asynchronous operation of our application:

await().atMost(2, SECONDS).untilAsserted(() -> {
  MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
  assertEquals(1, receivedMessages.length);
  MimeMessage receivedMessage = receivedMessages[0];
  assertEquals("Hello World!", GreenMailUtil.getBody(receivedMessage));
  assertEquals(1, receivedMessage.getAllRecipients().length);
  assertEquals("[email protected]", receivedMessage.getAllRecipients()[0].toString());

Using Testcontainers to start the GreenMail server

The GreenMail project also provides an official Docker image. In combination with Testcontainers, we can use this Docker image to start a local GreenMail Docker container for our integration tests.

We'll use the JUnit Jupiter extension from Testcontainers to manage the container lifecycle:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.15.1</version>
  <scope>test</scope>
</dependency>

Next, we define a GenericContainer using the official GreenMail Docker image. We can use the environment variable GREENMAIL_OPTS to tweak the email server configuration and add our service user. What's left is to tell Testcontainers which port of the GreenMail Docker container to expose.

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class NotificationControllerSecondIT {
  @Container
  static GenericContainer greenMailContainer = new GenericContainer<>(DockerImageName.parse("greenmail/standalone:1.6.1"))
    .withEnv("GREENMAIL_OPTS", "-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.users=duke:springboot")
    .withExposedPorts(3025);
  @DynamicPropertySource
  static void configureMailHost(DynamicPropertyRegistry registry) {
    registry.add("spring.mail.host", greenMailContainer::getHost);
    registry.add("spring.mail.port", greenMailContainer::getFirstMappedPort);
  @Autowired
  private TestRestTemplate testRestTemplate;
  @Test
  void shouldSendEmailWithCorrectPayloadToUser() throws Exception {
    String payload = "{ \"email\": \"[email protected]\", \"content\": \"Hello World!\"}";
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<String> request = new HttpEntity<>(payload, headers);
    ResponseEntity<Void> response = this.testRestTemplate.postForEntity("/notifications", request, Void.class);
    assertEquals(200, response.getStatusCodeValue());

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 prior to starting the Spring TestContext.

Verifying the emails with this approach becomes a little bit more tricky. We could create a JavaMail Sesion after the test execution and connect to the dockerized GreenMail instance.

However, this setup 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.

The source code for this demo on how to write integration tests for Spring's JavaMailSender with GreenMail and JUnit 5 is available on GitHub.

Have fun writing integration tests with GreenMail, Spring, and JUnit 5,

Philip

Want to learn more about testing? Join the Testing Java Applications Email Course

  • 14 Days Free E-Mail Course
  • Independent of the application framework
  • All participants receive the JUnit 5 & Mockito Cheat Sheet ($ 9.99)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK