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.M2

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 Writing the App

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

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

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

Due to the --features kotlin flag, it generates a Kotlin 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 Groovy.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

Kotlin, Kapt and IntelliJ

As of this writing IntelliJ’s built-in compiler does not directly support Kapt and annotation processing. You must instead configure Intellij to run Gradle (or Maven) compilation as a build step before running your tests or application class.

First edit the run configuration for tests or for the application and select "Run Gradle task" as a build step:

Intellij Settings

Then add the classes task as task to execute for the application or for tests the testClasses task:

Intellij Settings

Now whenever you run tests or the application Micronaut classes will be generated at compilation time.

Read Micronaut Kotlin section to learn more.

Alternatively, you can delegate IntelliJ build/run actions to gradle completely:

delegatetogradle

2.1 Controller

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

src/main/kotlin/example/micronaut/MailController.kt
package example.micronaut

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

@Controller("/mail") (1)
@Validated (2)
open class MailController(private val emailService: EmailService) {  (3)

    @Post("/send") (4)
    open fun send(@Body @Valid cmd: EmailCmd): HttpResponse<*> {  (5)
        emailService.send(cmd)
        return HttpResponse.ok<Any>() (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. Use a POGO supplied as a JSON payload in the request to populate the email.
6 Return 200 OK as the result

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

src/main/kotlin/example/micronaut/EmailCmd.kt
class EmailCmd : Email {

    @NotBlank
    @NotNull
    var recipient: String? = null

    @NotBlank
    @NotNull
    var subject: String? = null

    var cc: List<String>? = null

    var bcc: List<String>? = null

    var htmlBody: String? = null

    var textBody: String? = null

    var replyTo: String? = null

    override fun recipient(): String? {
        return this.recipient
    }

    override fun cc(): List<String>? {
        return this.cc
    }

    override fun bcc(): List<String>? {
        return this.bcc
    }

    override fun subject(): String? {
        return this.subject
    }

    override fun htmlBody(): String? {
        return this.htmlBody
    }

    override fun textBody(): String? {
        return this.textBody
    }

    override fun replyTo(): String? {
        return this.replyTo
    }

}

2.2 Email Service

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

src/kotlin/main/example/micronaut/EmailService.kt
package example.micronaut

interface EmailService {
    fun send(email: Email)
}
src/kotlin/main/example/micronaut/Email.kt
package example.micronaut

interface Email {
    fun recipient(): String?
    fun cc(): List<String>?
    fun bcc(): List<String>?
    fun subject(): String?
    fun htmlBody(): String?
    fun textBody(): String?
    fun replyTo(): String?
}

2.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/kotlin/example/micronaut/AwsCredentialsProviderCondition.kt
package example.micronaut

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

class AwsCredentialsProviderCondition : Condition {

    override fun matches(context: ConditionContext<*>): Boolean {
        return envOrSystemProperty("AWS_ACCESS_KEY_ID", "aws.accesskeyid") &&
                envOrSystemProperty("AWS_SECRET_KEY", "aws.secretkey")
    }

    private fun envOrSystemProperty(env: String, prop: String): Boolean {
        return notBlankAndNotNull(System.getProperty(prop)) || notBlankAndNotNull(System.getenv(env))
    }

    private fun notBlankAndNotNull(str: String?): Boolean {
        return str != null && str != ""
    }
}

Add a test:

src/test/kotlin/example/micronaut/AwsCredentialsProviderConditionSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class AwsCredentialsProviderConditionSpec: Spek({

    describe("AwsCredentialsProviderService loaded if condition") {
        val applicationContext = ApplicationContext.run("test")
        on("Verify AwsCredentialsProviderService is NOT loaded if system properties or environment properties are not set") {
            var exceptionThrown = false
            try {
                applicationContext.getBean(AwsCredentialsProviderService::class.java)
            } catch (e: NoSuchBeanException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }
//        on("Verify AwsCredentialsProviderService loaded if system properties are set") {
//            System.setProperty("aws.accesskeyid", "XXXX")
//            System.setProperty("aws.secretkey", "YYYY")
//            var exceptionThrown = false
//            try {
//                val awsCredentialsProviderService = applicationContext.getBean(AwsCredentialsProviderService::class.java)
//                assertTrue(awsCredentialsProviderService.secretKey == "YYYY")
//                assertTrue(awsCredentialsProviderService.accessKey == "XXXX")
//            } catch (e: NoSuchBeanException) {
//                exceptionThrown = true
//            }
//            assertFalse(exceptionThrown)
//
//            System.setProperty("aws.accesskeyid", null)
//            System.setProperty("aws.secretkey", null)
//        }
        afterGroup {
            applicationContext.close()
        }
    }
})
src/main/kotlin/example/micronaut/AwsCredentialsProviderService.kt
package example.micronaut

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

@Singleton (1)
@Requires(condition = AwsCredentialsProviderCondition::class)  (2)
class AwsCredentialsProviderService(@param:Value("\${AWS_ACCESS_KEY_ID:none}") val accessKeyEnv : String,  (3)
                                    @param:Value("\${AWS_SECRET_KEY:none}") val secretKeyEnv : String,
                                    @param:Value("\${aws.accesskeyid:none}") val accessKeyProp : String,
                                    @param:Value("\${aws.secretkey:none}") val secretKeyProp : String) : AWSCredentialsProvider {
    val accessKey : String
    val secretKey : String

    init {
        if (accessKeyEnv != "none") {
            accessKey = accessKeyEnv
        } else {
            accessKey = accessKeyProp
        }
        if (secretKeyEnv != "none") {
            secretKey = accessKeyEnv
        } else {
            secretKey = secretKeyProp
        }
    }
    override fun refresh() {
        TODO("not implemented")
    }

    override fun getCredentials(): AWSCredentials {
        return BasicAWSCredentials(accessKey, secretKey)
    }
}
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.

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/kotlin/example/micronaut/AwsSesMailCondition.kt
package example.micronaut

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

class AwsSesMailCondition : Condition {

    override fun matches(context: ConditionContext<*>): Boolean {
        return envOrSysPropNotBlankAndNotNull("AWS_SOURCE_EMAIL", "aws.sourceemail")
        &&  envOrSysPropNotBlankAndNotNull("AWS_REGION", "aws.region")
    }

    private fun envOrSysPropNotBlankAndNotNull(env: String?, prop: String?): Boolean {
        return notBlankAndNotNull(System.getProperty(prop)) ||
                notBlankAndNotNull(System.getenv(env))
    }

    private fun notBlankAndNotNull(str: String?): Boolean {
        return str != null && str != ""
    }
}

Add a test:

src/test/kotlin/example/micronaut/AwsSesMailConditionSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class AwsSesMailConditionSpec: Spek({

    describe("AwsSesMailService loaded if condition") {
        val applicationContext = ApplicationContext.run("test")
        on("Verify AwsSesMailService is NOT loaded if system properties or environment properties are not set") {
            var exceptionThrown = false
            try {
                applicationContext.getBean(AwsSesMailService::class.java)
            } catch (e: NoSuchBeanException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }
//        on("Verify AwsSesMailService loaded if system properties are set") {
//            System.setProperty("aws.region", "XXXX")
//            System.setProperty("aws.sourceemail", "me@micronaut.example")
//            var exceptionThrown = false
//            try {
//                applicationContext.getBean(AwsSesMailService::class.java)
//
//            } catch (e: NoSuchBeanException) {
//                exceptionThrown = true
//            }
//            assertFalse(exceptionThrown)
//        }
        afterGroup {
            applicationContext.close()
        }
    }
})
src/main/kotlin/example/micronaut/AwsSesMailService.kt
package example.micronaut

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 org.slf4j.LoggerFactory
import javax.inject.Singleton

@Singleton (1)
@Requires(condition = AwsSesMailCondition::class) (2)
@Primary (3)
class AwsSesMailService(@Value("\${AWS_REGION:none}") awsRegionEnv: String,  (4)
                        @Value("\${AWS_SOURCE_EMAIL:none}") sourceEmailEnv: String,
                        @Value("\${aws.region:none}") awsRegionProp: String,
                        @Value("\${aws.sourceemail:none}") sourceEmailProp: String,
                        private val awsCredentialsProviderService: AwsCredentialsProviderService?) : EmailService {

    private val awsRegion: String = if (awsRegionEnv != "none") awsRegionEnv else awsRegionProp
    private val sourceEmail: String = if (sourceEmailEnv != "none") sourceEmailEnv else sourceEmailProp

    private fun bodyOfEmail(email: Email): Body {
        if (email.htmlBody() != null && !email.htmlBody()!!.isEmpty()) {
            val htmlBody = Content().withData(email.htmlBody())
            return Body().withHtml(htmlBody)
        }
        if (email.textBody() != null && !email.textBody()!!.isEmpty()) {
            val textBody = Content().withData(email.textBody())
            return Body().withHtml(textBody)
        }
        return Body()
    }

    override fun send(email: Email) {

        if (awsCredentialsProviderService == null) {
            if (LOG.isWarnEnabled) {
                LOG.warn("AWS Credentials provider not configured")
            }
            return
        }

        var destination = Destination().withToAddresses(email.recipient())
        if (email.cc() != null) {
            destination = destination.withCcAddresses(email.cc())
        }
        if (email.bcc() != null) {
            destination = destination.withBccAddresses(email.bcc())
        }
        val subject = Content().withData(email.subject())
        val body = bodyOfEmail(email)
        val message = Message().withSubject(subject).withBody(body)

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

        if (email.replyTo() != null) {
            request = request.withReplyToAddresses()
        }

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

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

            val sendEmailResult = client.sendEmail(request)

            if (LOG.isInfoEnabled) {
                LOG.info("Email sent! {}", sendEmailResult.toString())
            }
        } catch (ex: Exception) {
            if (LOG.isWarnEnabled) {
                LOG.warn("The email was not sent.")
                LOG.warn("Error message: {}", ex.message)
            }
        }
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(AwsSesMailService::class.java)
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 @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.
4 Values will be resolved from system properties.

Add a test:

src/test/kotlin/example/micronaut/AwsSesMailServiceSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class AwsSesMailServiceSpec: Spek({

    describe("AwsSesMailService load") {
        val applicationContext = ApplicationContext.run("test")
        on("AwsSesMailService is not loaded if system property is not present") {
            var exceptionThrown = false
            try {
                applicationContext.getBean(AwsSesMailService::class.java)
            } catch (e: NoSuchBeanException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }
//        on("Verify AwsSesMailService loaded if system properties are set") {
//        System.setProperty("aws.region", "XXXX")
//        System.setProperty("aws.sourceemail", "me@micronaut.example")
//        System.setProperty("aws.accesskeyid", "XXXX")
//        System.setProperty("aws.secretkey", "YYYY")
//            var exceptionThrown = false
//            try {
//                applicationContext.getBean(AwsSesMailService::class.java)
//
//            } catch (e: NoSuchBeanException) {
//                exceptionThrown = true
//            }
//            assertFalse(exceptionThrown)
//        }
        afterGroup {
            applicationContext.close()
        }
    }
})

2.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/kotlin/example/micronaut/SendGridEmailCondition.kt
package example.micronaut

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

class SendGridEmailCondition : Condition {
    override fun matches(context: ConditionContext<*>?): Boolean {
        return envOrSysPropNotBlankAndNotNull("SENDGRID_APIKEY", "sendgrid.apikey")
                &&  envOrSysPropNotBlankAndNotNull("SENDGRID_FROM_EMAIL", "sendgrid.fromemail")
    }

    private fun envOrSysPropNotBlankAndNotNull(env: String?, prop: String?): Boolean {
        return notBlankAndNotNull(System.getProperty(prop)) ||
                notBlankAndNotNull(System.getenv(env))
    }

    private fun notBlankAndNotNull(str: String?): Boolean {
        return str != null && str != ""
    }

}

Add a test:

src/test/kotlin/example/micronaut/SendGridEmailConditionSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class SendGridEmailConditionSpec: Spek({

    describe("SendGridEmailService loaded if condition") {
        val applicationContext = ApplicationContext.run("test")
        on("Verify SendGridEmailService is NOT loaded if system properties or environment properties are not set") {
            var exceptionThrown = false
            try {
                applicationContext.getBean(SendGridEmailService::class.java)
            } catch (e: NoSuchBeanException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }
//        on("Verify SendGridEmailService loaded if system properties are set") {
//        System.setProperty("sendgrid.apikey", "XXXX")
//        System.setProperty("sendgrid.fromemail", "me@micronaut.example")
//            var exceptionThrown = false
//            try {
//                applicationContext.getBean(SendGridEmailService::class.java)
//
//            } catch (e: NoSuchBeanException) {
//                exceptionThrown = true
//            }
//            assertFalse(exceptionThrown)
//        }
        afterGroup {
            applicationContext.close()
        }
    }
})
src/main/kotlin/example/micronaut/SendGridEmailService.kt
package example.micronaut

import com.sendgrid.Content
import com.sendgrid.Mail
import com.sendgrid.Method
import com.sendgrid.Personalization
import com.sendgrid.Request
import com.sendgrid.SendGrid
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import org.slf4j.LoggerFactory
import javax.inject.Singleton
import java.io.IOException

@Singleton (1)
@Requires(condition = SendGridEmailCondition::class) (2)
class SendGridEmailService(@Value("\${SENDGRID_APIKEY:none}") apiKeyEnv: String,  (3)
                                    @Value("\${SENDGRID_FROM_EMAIL:none}") fromEmailEnv: String,
                                    @Value("\${sendgrid.apikey:none}") apiKeyProp: String,
                                    @Value("\${sendgrid.fromemail:none}") fromEmailProp: String) : EmailService {
    protected val apiKey: String
    protected val fromEmail: String

    init {
        this.apiKey = if (apiKeyEnv != "none") apiKeyEnv else apiKeyProp
        this.fromEmail = if (fromEmailEnv != "none") fromEmailEnv else fromEmailProp
    }

    protected fun contentOfEmail(email: Email): Content? {
        if (email.textBody() != null) {
            return Content("text/plain", email.textBody())
        }
        return if (email.htmlBody() != null) {
            Content("text/html", email.htmlBody())
        } else null
    }

    override fun send(email: Email) {

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

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

        if (email.cc() != null) {
            for (cc in email.cc()!!) {
                val ccEmail = com.sendgrid.Email()
                ccEmail.email = cc
                personalization.addCc(ccEmail)
            }
        }

        if (email.bcc() != null) {
            for (bcc in email.bcc()!!) {
                val bccEmail = com.sendgrid.Email()
                bccEmail.email = bcc
                personalization.addBcc(bccEmail)
            }
        }

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

        val sg = SendGrid(apiKey)
        val request = Request()
        try {
            request.method = Method.POST
            request.endpoint = "mail/send"
            request.body = mail.build()

            val response = sg.api(request)
            if (LOG.isInfoEnabled) {
                LOG.info("Status Code: {}", response.statusCode.toString())
                LOG.info("Body: {}", response.body)
                for (k in response.headers.keys) {
                    val v = response.headers[k]
                    LOG.info("Response Header {} => {}", k, v)
                }
            }


        } catch (ex: IOException) {
            if (LOG.isErrorEnabled) {
                LOG.error(ex.message)
            }
        }
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(SendGridEmailService::class.java)
    }
}
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.

2.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 system properties, the AWS SES EmailService implementation will be used due to the @Primary annotation.

2.4 Test

Speck is a Kotlin Specification Framework for the JVM.

To use Spek modify build.gradle file as described in the Spek documentation:

build.gradle
buildscript {
    repositories {
      ...
      ..
    }
    dependencies {
      ...
      ..
        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.2.0'
    }
}
...
..
.
apply plugin: 'org.junit.platform.gradle.plugin'

junitPlatform {
    filters {
        engines {
            include 'spek'
        }
    }
}

repositories {
  ...
  ..
    maven { url "https://jcenter.bintray.com" }
}

dependencies {
...
..
    testCompile "org.jetbrains.spek:spek-api:1.1.5"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:1.1.5"
    testCompile 'org.jetbrains.kotlin:kotlin-test:1.1.0'
}
...
..

test {
    useJUnitPlatform()
}

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/kotlin/example/micronaut/MockEmailService.kt
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 : EmailService {
    val emails = mutableListOf<Email>()

    override fun send(email: Email) {
        emails.add(email)
    }
}

Create the next test:

src/test/kotlin/example/micronaut/MailControllerSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.reactivex.Flowable
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class MailControllerSpec: Spek({

    describe("MailController") {

        var embeddedServer : EmbeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "mailcontroller"),
                "test")  (1)
        var client : RxHttpClient = RxHttpClient.create(embeddedServer.url)  (2)

        on("/mail/send interacts once email service") {
            val cmd = EmailCmd()
            cmd.subject = "Test"
            cmd.recipient = "delamos@grails.example"
            cmd.textBody = "Hola hola"

            val request = HttpRequest.POST("/mail/send", cmd) (3)

            var emailServices = embeddedServer.applicationContext.getBeansOfType(EmailService::class.java)

            assertTrue(emailServices.size == 1)

            var emailService = embeddedServer.applicationContext.getBean(EmailService::class.java)

            assertTrue(emailService is MockEmailService)

            val oldcount : Int  = (emailService as MockEmailService).emails.size

            val rsp : HttpResponse<Any> = client.toBlocking().exchange(request)

            assertEquals(rsp.status, HttpStatus.OK)

            val count : Int  = (emailService as MockEmailService).emails.size
            assertEquals(count, oldcount + 1) (4)
        }

        afterGroup {
            client.close()
            embeddedServer.close()
        }

    }
})
1 To run the application from a unit test you can use the EmbeddedServer interface
2 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.
3 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
4 emailService.send method is invoked once.

2.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/kotlin/example/micronaut/MailControllerValidationSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.on
import org.jetbrains.spek.api.dsl.xdescribe
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class MailControllerValidationSpec: Spek({

    describe("MailController Validation") {
        var embeddedServer : EmbeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "mailcontroller"),
                "test")  (1)
        var client : RxHttpClient = RxHttpClient.create(embeddedServer.url)  (2)

        on("/mail/send cannot be invoked without subject") {
            val cmd = EmailCmd()
            cmd.recipient = "delamos@micronaut.example"
            cmd.textBody = "Hola hola"

            val request = HttpRequest.POST("/mail/send", cmd) (3)

            var exceptionThrown = false
            try {
                client.toBlocking().exchange(request, HttpResponse::class.java)
            } catch (e: HttpClientResponseException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }

        on("/mail/send cannot be invoked without recipient") {
            val cmd = EmailCmd()
            cmd.subject = "Hola"
            cmd.textBody = "Hola hola"
            val request = HttpRequest.POST("/mail/send", cmd) (3)

            var exceptionThrown = false
            try {
                client.toBlocking().exchange(request, HttpResponse::class.java)
            } catch (e: HttpClientResponseException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }

        on("/mail/send cannot be invoked without either textBody or htmlBody") {
            val cmd = EmailCmd()
            cmd.subject = "Hola"
            cmd.recipient = "delamos@micronaut.example"
            val request = HttpRequest.POST("/mail/send", cmd) (3)

            var exceptionThrown = false
            try {
                client.toBlocking().exchange(request, HttpResponse::class.java)
            } catch (e: HttpClientResponseException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }

//        on("/mail/send can be invoked with textBody and not htmlBody") {
//            val cmd = EmailCmd()
//            cmd.subject = "Hola"
//            cmd.recipient = "delamos@micronaut.example"
//            cmd.textBody = "Hello"
//
//            val request = HttpRequest.POST("mail/send", cmd) (3)
//
//            val rsp = client.toBlocking().exchange(request, HttpResponse::class.java)
//
//            assertEquals(rsp.status(), HttpStatus.OK)
//       }
//
//        on("/mail/send can be invoked with htmlBody and not textBody") {
//            val cmd = EmailCmd()
//            cmd.subject = "Hola"
//            cmd.recipient = "delamos@micronaut.example"
//            cmd.htmlBody = "<h1>Hello</h1>"
//            val request = HttpRequest.POST("/mail/send", cmd) (3)
//
//            val rsp = client.toBlocking().exchange(request, HttpResponse::class.java)
//
//            assertEquals(rsp.status(), HttpStatus.OK)
//        }

        afterGroup {
            client.close()
            embeddedServer.close()
        }
    }
})
1 To run the application from a unit test you can use the EmbeddedServer interface
2 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.
3 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.

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

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

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)
public @interface EmailConstraints {

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

}

and a validator:

src/main/java/example/micronaut/EmailConstraintsValidator.java
package example.micronaut;

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

public class EmailConstraintsValidator implements ConstraintValidator<EmailConstraints, EmailCmd> {

    @Override
    public boolean isValid(EmailCmd email, ConstraintValidatorContext context) {
        return email !=null &&
                (notBlankAndNotNull(email.getTextBody()) || notBlankAndNotNull(email.getHtmlBody()));
    }

    private boolean notBlankAndNotNull(String str) {
        return str != null && !str.equals("");
    }
}

Annotate Email with EmailConstraints.

src/main/kotlin/example/micronaut/EmailCmd.kt
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

@EmailConstraints
class EmailCmd : Email {

    @NotBlank
    @NotNull
    var recipient: String? = null

    @NotBlank
    @NotNull
    var subject: String? = null

    var cc: List<String>? = null

    var bcc: List<String>? = null

    var htmlBody: String? = null

    var textBody: String? = null

    var replyTo: String? = null

    override fun recipient(): String? {
        return this.recipient
    }

    override fun cc(): List<String>? {
        return this.cc
    }

    override fun bcc(): List<String>? {
        return this.bcc
    }

    override fun subject(): String? {
        return this.subject
    }

    override fun htmlBody(): String? {
        return this.htmlBody
    }

    override fun textBody(): String? {
        return this.textBody
    }

    override fun replyTo(): String? {
        return this.replyTo
    }

}

3 Testing the Application

To run the tests:

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