Send Emails from Micronaut

Learn how to send emails with AWS SES and SendGrid from a Micronaut app and leverage @Requires annotation to load beans conditionally.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M4

1 Getting Started

In this guide we are going to create a Micronaut app written in Groovy.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Application

Remove the Application.java created by the mn create-app --features groovy command.

Create a Groovy file instead which is used when running the application via Gradle or via deployment. You can also run the main class directly within your IDE if it is configured correctly.

src/main/groovy/example/micronaut/Application.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.runtime.Micronaut

@CompileStatic
class Application {

    static void main(String[] args) {
        Micronaut.run(Application.class)
    }
}

3 Writing the App

Create a Groovy Micronaut app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete --features=groovy

The previous command createas a micronaut app with the default package example.micronaut in a folder named complete.

Due to the --features groovy flag, it generates a Groovy Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Java or Kotlin.

3.1 Controller

Create MailController which use a collaborator, emailService to send and email.

src/main/groovy/example/micronaut/MailController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import javax.validation.Valid

@Slf4j
@CompileStatic
@Controller('/mail') (1)
@Validated (2)
class MailController {

    EmailService emailService

    MailController( EmailService  emailService) { (3)
        this.emailService = emailService
    }


    @Post('/send') (4)
    HttpResponse send(@Body @Valid EmailCmd cmd) { (5)
        log.info '{}', cmd.toString()
        emailService.send(cmd)
        HttpResponse.ok()  (6)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /mail/send
2 Add @Validated annotation at the class level to any class that requires validation.
3 Constructor injection
4 The @Post annotation is used to map the index method to all requests that use an HTTP POST
5 Add @Valid to any method parameter which requires validation.
6 Use a POGO supplied as a JSON payload in the request to populate the email.
7 Return 200 OK as the result

The previous controller uses a POGO supplied in the request body as a JSON Payload

src/main/groovy/example/micronaut/EmailCmd.groovy
@ToString
@CompileStatic
class EmailCmd implements Email {

    @NotNull
    @NotBlank
    String recipient

    @NotNull
    @NotBlank
    String subject

    List<String> cc = []
    List<String> bcc = []
    String htmlBody
    String textBody
    String replyTo

}

3.2 Email Service

Create an interface - EmailService. Any email provider present in the application should implement it.

src/groovy/main/example/micronaut/EmailService.groovy
package example.micronaut

import groovy.transform.CompileStatic

@CompileStatic
interface EmailService {
    void send(Email email)
}
src/groovy/main/example/micronaut/Email.groovy
package example.micronaut

import groovy.transform.CompileStatic

@CompileStatic
interface Email {
    String getRecipient()
    List<String> getCc()
    List<String> getBcc()
    String getSubject()
    String getHtmlBody()
    String getTextBody()
    String getReplyTo()
}

3.2.1 AWS SES

Amazon Simple Email Service (Amazon SES) is a cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. It is a reliable, cost-effective service for businesses of all sizes that use email to keep in contact with their customers.

Add a dependency to AWS SES SDK:

build.gradle
    compile 'com.amazonaws:aws-java-sdk-ses:1.11.285'

Create a service which encapsulates AWS credentials provider. The bean will not be loaded if the environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_KEY) or system properties ( aws.accesskeyid, aws.secretkey) are not present.

src/main/groovy/example/micronaut/AwsCredentialsProviderCondition.groovy
package example.micronaut

import io.micronaut.context.condition.Condition
import io.micronaut.context.condition.ConditionContext

class AwsCredentialsProviderCondition implements Condition {
    @Override
    boolean matches(ConditionContext context) {
        (System.getProperty("aws.accesskeyid") || System.getenv("AWS_ACCESS_KEY_ID")) &&
                (System.getProperty("aws.secretkey") ||  System.getenv("AWS_SECRET_KEY"))
    }
}

Add a test:

src/test/groovy/example/micronaut/AwsCredentialsProviderConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsCredentialsProviderConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")

        AwsCredentialsProviderCondition condition = new AwsCredentialsProviderCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        AwsCredentialsProviderCondition condition = new AwsCredentialsProviderCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/groovy/example/micronaut/AwsCredentialsProviderService.groovy
package example.micronaut

import com.amazonaws.auth.AWSCredentials
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import javax.inject.Singleton

@Singleton (1)
@Requires(condition = AwsCredentialsProviderCondition) (2)
@CompileStatic
class AwsCredentialsProviderService implements AWSCredentialsProvider {

    String accessKey
    String secretKey

    AwsCredentialsProviderService(@Value('${AWS_ACCESS_KEY_ID:none}') String accessKeyEnv, (3)
                                  @Value('${AWS_SECRET_KEY:none}') String secretKeyEnv,
                                  @Value('${aws.accesskeyid:none}') String accessKeyProp,
                                  @Value('${aws.secretkey:none}') String secretKeyProp) {
        this.accessKey = accessKeyEnv != null && !accessKeyEnv.equals("none") ? accessKeyEnv : accessKeyProp;
        this.secretKey = secretKeyEnv != null && !secretKeyEnv.equals("none")  ? accessKeyEnv : secretKeyProp;
    }

    @Override
    AWSCredentials getCredentials() {
        return new BasicAWSCredentials(accessKey, secretKey)
    }

    @Override
    void refresh() {

    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/AwsCredentialsProviderServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsCredentialsProviderServiceSpec extends Specification {

    def "AwsCredentialsProviderService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "AwsCredentialsProviderService is loaded if system property is not present"() {
        given:
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        AwsCredentialsProviderService bean = embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)

        then:
        noExceptionThrown()
        bean.secretKey == 'YYYY'
        bean.accessKey == 'XXXX'

        cleanup:
        embeddedServer.close()
    }
}

Create a service which sends the email with AWS Simple Email Service. The bean will not be loaded if the environment variables (AWS_REGION, AWS_SOURCE_EMAIL) or system properties (aws.region, aws.sourceemail) are not present.

src/main/groovy/example/micronaut/AwsSesMailCondition.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.context.condition.Condition
import io.micronaut.context.condition.ConditionContext

@CompileStatic
class AwsSesMailCondition implements Condition {

    @Override
    boolean matches(ConditionContext context) {
        (System.getProperty("aws.sourceemail") || System.getenv("AWS_SOURCE_EMAIL")) &&
                (System.getProperty("aws.region") ||  System.getenv("AWS_REGION"))
    }
}

Add a test:

src/test/groovy/example/micronaut/AwsSesMailConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsSesMailConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("aws.region", "XXXX")
        System.setProperty("aws.sourceemail", "me@micronaut.example")
        AwsSesMailCondition condition = new AwsSesMailCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        AwsSesMailCondition condition = new AwsSesMailCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/groovy/example/micronaut/AwsSesMailService.groovy
package example.micronaut
import com.amazonaws.services.simpleemail.model.SendEmailResult
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder
import com.amazonaws.services.simpleemail.model.Body
import com.amazonaws.services.simpleemail.model.Content
import com.amazonaws.services.simpleemail.model.Destination
import com.amazonaws.services.simpleemail.model.Message
import com.amazonaws.services.simpleemail.model.SendEmailRequest
import io.micronaut.context.annotation.Primary
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import javax.inject.Singleton


@Singleton (1)
@Requires(beans = AwsCredentialsProviderService) (2)
@Requires(condition = AwsSesMailCondition)  (3)
@Primary (4)
@Slf4j
@CompileStatic
class AwsSesMailService implements EmailService {

    String awsRegion

    String sourceEmail

    AwsCredentialsProviderService awsCredentialsProviderService

    AwsSesMailService(@Value('${AWS_REGION:none}') String awsRegionEnv, (5)
                      @Value('${AWS_SOURCE_EMAIL:none}') String sourceEmailEnv,
                      @Value('${aws.region:none}') String awsRegionProp,
                      @Value('${aws.sourceemail:none}') String sourceEmailProp,
                      AwsCredentialsProviderService awsCredentialsProviderService
    ) {
        this.awsRegion = awsRegionEnv != null && !awsRegionEnv.equals("none") ? awsRegionEnv : awsRegionProp
        this.sourceEmail = sourceEmailEnv != null && !sourceEmailEnv.equals("none") ? sourceEmailEnv : sourceEmailProp
        this.awsCredentialsProviderService = awsCredentialsProviderService
    }

    private Body bodyOfEmail(Email email) {
        if (email.htmlBody) {
            Content htmlBody = new Content().withData(email.htmlBody)
            return new Body().withHtml(htmlBody)
        }
        if (email.textBody) {
            Content textBody = new Content().withData(email.textBody)
            return new Body().withHtml(textBody)
        }
        new Body()
    }

    @Override
    void send(Email email) {

        if ( !awsCredentialsProviderService ) {
            log.warn("AWS Credentials provider not configured")
            return
        }

        Destination destination = new Destination().withToAddresses(email.recipient)
        if ( email.getCc() ) {
            destination = destination.withCcAddresses(email.getCc())
        }
        if ( email.getBcc() ) {
            destination = destination.withBccAddresses(email.getBcc())
        }
        Content subject = new Content().withData(email.getSubject())
        Body body = bodyOfEmail(email)
        Message message = new Message().withSubject(subject).withBody(body)

        SendEmailRequest request = new SendEmailRequest()
                .withSource(sourceEmail)
                .withDestination(destination)
                .withMessage(message)

        if ( email.getReplyTo() ) {
            request = request.withReplyToAddresses()
        }

        try {
            log.info("Attempting to send an email through Amazon SES by using the AWS SDK for Java...")

            AmazonSimpleEmailService client = AmazonSimpleEmailServiceClientBuilder.standard()
                    .withCredentials(awsCredentialsProviderService)
                    .withRegion(awsRegion)
                    .build()

            SendEmailResult sendEmailResult = client.sendEmail(request)
            log.info("Email sent! {}", sendEmailResult.toString())

        } catch (Exception ex) {
            log.warn("The email was not sent.")
            log.warn("Error message: {}", ex.message)
        }
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Bean will not loaded, if bean is not present.
4 @Primary is a qualifier that indicates that a bean is the primary bean that should be selected in the case of multiple possible interface implementations.
5 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/AwsSesMailServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsSesMailServiceSpec extends Specification {

    def "AwsSesMailService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsSesMailService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "AwsSesMailService is loaded if system property is not present"() {
        given:
        System.setProperty("aws.region", "XXXX")
        System.setProperty("aws.sourceemail", "me@micronaut.example")
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)
        AwsSesMailService bean = embeddedServer.applicationContext.getBean(AwsSesMailService)

        then:
        noExceptionThrown()
        bean.sourceEmail == 'me@micronaut.example'
        bean.awsRegion == 'XXXX'

        cleanup:
        embeddedServer.close()
    }
}

3.2.2 SendGrid

SendGrid is a transactional email service.

SendGrid is responsible for sending billions of emails for some of the best and brightest companies in the world.

Add a dependency to SendGrid SDK:

build.gradle
    compile 'com.sendgrid:sendgrid-java:4.1.2'

Create a service which encapsulates the integration with SendGrid. The bean will not be loaded if the system properties (sendgrid.apikey, sendgrid.fromemail) or environment variables (SENDGRID_APIKEY, SENDGRID_FROM_EMAIL) are not present.

src/main/groovy/example/micronaut/SendGridEmailCondition.groovy
package example.micronaut

import io.micronaut.context.condition.Condition
import io.micronaut.context.condition.ConditionContext

class SendGridEmailCondition implements Condition {

    @Override
    boolean matches(ConditionContext context) {
        (System.getProperty("sendgrid.apikey") || System.getenv("SENDGRID_APIKEY")) &&
                (System.getProperty("sendgrid.fromemail") ||  System.getenv("SENDGRID_FROM_EMAIL"))
    }
}

Add a test:

src/test/groovy/example/micronaut/SendGridEmailConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class SendGridEmailConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("sendgrid.apikey", "XXXX")
        System.setProperty("sendgrid.fromemail", "me@micronaut.example")
        SendGridEmailCondition condition = new SendGridEmailCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        SendGridEmailCondition condition = new SendGridEmailCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/groovy/example/micronaut/SendGridEmailService.groovy
package example.micronaut
import com.sendgrid.Personalization
import com.sendgrid.Content
import com.sendgrid.Mail
import com.sendgrid.SendGrid
import com.sendgrid.Request
import com.sendgrid.Response
import com.sendgrid.Method
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import javax.inject.Singleton

@Singleton (1)
@Requires(condition = SendGridEmailCondition) (2)
@Slf4j
@CompileStatic
class SendGridEmailService implements EmailService {

    String apiKey

    String fromEmail

    SendGridEmailService(@Value('${SENDGRID_APIKEY:none}') String apiKeyEnv, (3)
                         @Value('${SENDGRID_FROM_EMAIL:none}') String fromEmailEnv,
                         @Value('${sendgrid.apikey:none}') String apiKeyProp,
                         @Value('${sendgrid.fromemail:none}') String fromEmailProp) {
        this.apiKey = apiKeyEnv != null && !apiKeyEnv.equals("none") ? apiKeyEnv : apiKeyProp;
        this.fromEmail = fromEmailEnv != null && !fromEmailEnv.equals("none")  ? fromEmailEnv: fromEmailProp;
    }


    protected Content contentOfEmail(Email email) {
        if ( email.textBody ) {
            return new Content("text/plain", email.textBody)
        }
        if ( email.htmlBody ) {
            return new Content("text/html", email.htmlBody)
        }
        return null
    }

    @Override
    void send(Email email) {

        Personalization personalization = new Personalization()
        personalization.subject = email.subject

        com.sendgrid.Email to = new com.sendgrid.Email(email.recipient)
        personalization.addTo(to)

        if ( email.getCc() ) {
            for ( String cc : email.getCc() ) {
                com.sendgrid.Email ccEmail = new com.sendgrid.Email()
                ccEmail.email = cc
                personalization.addCc(ccEmail)
            }
        }

        if ( email.getBcc() ) {
            for ( String bcc : email.getBcc() ) {
                com.sendgrid.Email bccEmail = new com.sendgrid.Email()
                bccEmail.email = bcc
                personalization.addBcc(bccEmail)
            }
        }

        Mail mail = new Mail()
        com.sendgrid.Email from = new com.sendgrid.Email()
        from.email = fromEmail
        mail.from = from
        mail.addPersonalization(personalization)
        Content content = contentOfEmail(email)
        mail.addContent(content)

        SendGrid sg = new SendGrid(apiKey)
        Request request = new Request()
        try {
            request.with {
                method = Method.POST
                endpoint = "mail/send"
                body = mail.build()
            }
            Response response = sg.api(request)
            log.info("Status Code: {}", String.valueOf(response.getStatusCode()))
            log.info("Body: {}", response.getBody())
            if ( log.infoEnabled ) {
                response.getHeaders().each { String k, String v ->
                    log.info("Response Header {} => {}", k, v)
                }
            }

        } catch (IOException ex) {
            log.error(ex.getMessage())
        }
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/SendGridEmailServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class SendGridEmailServiceSpec extends Specification {

    def "SendGridEmailService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(SendGridEmailService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "SendGridEmailService is loaded if system property is not present"() {
        given:
        System.setProperty("sendgrid.apikey", "XXXX")
        System.setProperty("sendgrid.fromemail", "me@micronaut.example")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        SendGridEmailService bean = embeddedServer.applicationContext.getBean(SendGridEmailService)

        then:
        noExceptionThrown()
        bean.apiKey == 'XXXX'
        bean.fromEmail == 'me@micronaut.example'

        cleanup:
        embeddedServer.close()
    }
}

3.3 Run the app

Add a logger to get more visibility:

src/main/resources/logback.xml
...
..
.
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>

    <logger name="example.micronaut" level="TRACE"/>

</configuration>

To use SendGrid, setup the required environment variables and run the app

$ export SENDGRID_FROM_EMAIL=email@email.com
$ export SENDGRID_APIKEY=XXXXXX
$ ./gradlew run

To use AWS SES, setup the required environment variables and run the app

$ export AWS_REGION=eu-west-1
$ export AWS_SOURCE_EMAIL=email@email.com
$ export AWS_ACCESS_KEY_ID=XXXXXXXX
$ export AWS_SECRET_KEY=XXXXXXXX
$ ./gradlew run

If you supply both AWS SES and SendGrid environment variables, the AWS SES EmailService implementation will be used due to the @Primary annotation.

3.4 Test

In our acceptance test, beans SendGridEmailService or AwsSesMailService will not be loaded since system properties are not present.

Instead, we setup a Mock which we can verify interactions against.

src/test/groovy/example/micronaut/MockEmailService.groovy
package example.micronaut

import io.micronaut.context.annotation.Primary
import io.micronaut.context.annotation.Requires

import javax.inject.Singleton

@Primary
@Requires(property = 'spec.name', value = 'mailcontroller')
@Singleton
class MockEmailService implements EmailService {

    List<Email> emails = []

    @Override
    void send(Email email) {
        emails << email
    }
}

Create the next test:

src/test/groovy/example/micronaut/MailControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

import javax.annotation.Nullable

class MailControllerSpec extends Specification {

    @Shared
    @AutoCleanup (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
            'spec.name': 'mailcontroller',
    ], 'test')  (2)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) (3)

    def "/mail/send interacts once email service"() {
        given:
        EmailCmd cmd = new EmailCmd(subject: 'Test',
                recipient: 'delamos@grails.example',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        Collection emailServices = embeddedServer.applicationContext.getBeansOfType(EmailService)

        then:
        !emailServices.any { it == SendGridEmailService.class}
        !emailServices.any { it == AwsSesMailService.class}

        when:
        EmailService emailService = embeddedServer.applicationContext.getBean(EmailService)

        then:
        emailService instanceof MockEmailService

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status.code == 200
        ((MockEmailService)emailService).emails.size() == old(((MockEmailService)emailService).emails.size()) + 1 (5)
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 emailService.send method is invoked once.

3.5 Validation

We want to ensure any email request contains a subject, recipient and a text body or html body.

Micronaut’s validation is built on with the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API.

Add a dependency to it:

build.gradle
    compile "io.micronaut:validation"
    compile "io.micronaut.configuration:hibernate-validator"

Create the next test:

src/test/java/example/micronaut/MailControllerValidationSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification


class MailControllerValidationSpec extends Specification {

    @Shared
    @AutoCleanup
    (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
            'spec.name': 'mailcontroller',
    ], 'test')  (2)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) (3)


    def "/mail/send cannot be invoked without subject"() {
        given:
        EmailCmd cmd = new EmailCmd(
                recipient: 'delamos@micronaut.example',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send cannot be invoked without recipient"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send cannot be invoked without either textBody or htmlBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',)
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send can be invoked without textBody and not htmlBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',
                textBody: 'Hello')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status().code == 200
    }

    def "/mail/send can be invoked without htmlBody and not textBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',
                htmlBody: '<h1>Hello</h1>')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status().code == 200
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.

In order to satisfy the test, create an email constraints annotation

src/main/groovy/example/micronaut/EmailConstraints.java
package example.micronaut;

import groovy.transform.CompileStatic;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = { EmailConstraintsValidator.class })
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@CompileStatic
@interface EmailConstraints {

    String message() default "{email.invalid}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

and a validator:

src/main/groovy/example/micronaut/EmailConstraintsValidator.groovy
package example.micronaut

import groovy.transform.CompileStatic

import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext

@CompileStatic
class EmailConstraintsValidator implements ConstraintValidator<EmailConstraints, EmailCmd> {

    @Override
    boolean isValid(EmailCmd email, ConstraintValidatorContext context) {
        email && (email.textBody || email.htmlBody)
    }
}

Annotate Email with EmailConstraints and populate the hasErrors method.

src/main/java/example/micronaut/EmailCmd.groovy
import groovy.transform.CompileStatic
import groovy.transform.ToString
import javax.validation.Validation
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

@ToString
@CompileStatic
@EmailConstraints (1)
class EmailCmd implements Email {

    @NotNull
    @NotBlank
    String recipient

    @NotNull
    @NotBlank
    String subject

    List<String> cc = []
    List<String> bcc = []
    String htmlBody
    String textBody
    String replyTo
}

4 Testing the Application

To run the tests:

$ ./gradlew test
$ open build/reports/tests/test/index.html