mn create-app example.micronaut.micronautguide --build=gradle --lang=java
Send Emails from the Micronaut framework
Learn how to send emails with AWS SES and SendGrid from a Micronaut application and leverage @Requires annotation to load beans conditionally.
Authors: Sergio del Amo
Micronaut Version: 3.3.0
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
2. 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
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
4.1. Enable annotation Processing
If you use Java or Kotlin and IntelliJ IDEA, make sure to enable annotation processing.
4.2. Controller
Create MailController
which use a collaborator, emailService
to send and email.
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.Valid;
@Controller("/mail") (1)
public class MailController {
private final EmailService emailService;
public MailController(EmailService emailService) { (2)
this.emailService = emailService;
}
@Post("/send") (3)
public HttpResponse<?> send(@Body @Valid EmailCmd cmd) { (4)
emailService.send(cmd);
return HttpResponse.ok(); (5)
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /mail/send |
2 | Constructor injection |
3 | The @Post annotation maps the index method to all requests that use an HTTP POST |
4 | Add @Valid to any method parameter which requires validation. Use a POGO supplied as a JSON payload in the request to populate the email. |
5 | Return 200 OK as the result |
The previous controller uses a POJO supplied in the request body as a JSON Payload
@Introspected
public class EmailCmd implements Email {
@NotNull
@NotBlank
private String recipient;
@NotNull
@NotBlank
private String subject;
private List<String> cc = new ArrayList<>();
private List<String> bcc = new ArrayList<>();
private String htmlBody;
private String textBody;
private String replyTo;
@Override
public String getRecipient() {
return recipient;
}
public void setRecipient(String recipient) {
this.recipient = recipient;
}
@Override
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
@Override
public List<String> getCc() {
return cc;
}
public void setCc(List<String> cc) {
this.cc = cc;
}
@Override
public List<String> getBcc() {
return bcc;
}
public void setBcc(List<String> bcc) {
this.bcc = bcc;
}
@Override
public String getHtmlBody() {
return htmlBody;
}
public void setHtmlBody(String htmlBody) {
this.htmlBody = htmlBody;
}
@Override
public String getTextBody() {
return textBody;
}
public void setTextBody(String textBody) {
this.textBody = textBody;
}
@Override
public String getReplyTo() {
return replyTo;
}
public void setReplyTo(String replyTo) {
this.replyTo = replyTo;
}
}
4.3. Email Service
Create an interface - EmailService
. Any email provider present in the application should implement it.
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public interface EmailService {
void send(@NonNull @NotNull @Valid Email email);
}
package example.micronaut;
import java.util.List;
public interface Email {
String getRecipient();
List<String> getCc();
List<String> getBcc();
String getSubject();
String getHtmlBody();
String getTextBody();
String getReplyTo();
}
4.3.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:
implementation("software.amazon.awssdk:ses:2.17.124")
Create service which uses AWS Simple Email Service client to send the email
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Secondary;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ses.SesClient;
import software.amazon.awssdk.services.ses.model.Body;
import software.amazon.awssdk.services.ses.model.Content;
import software.amazon.awssdk.services.ses.model.Destination;
import software.amazon.awssdk.services.ses.model.Message;
import software.amazon.awssdk.services.ses.model.SendEmailRequest;
import software.amazon.awssdk.services.ses.model.SendEmailResponse;
import jakarta.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Singleton (1)
@Requires(condition = AwsResourceAccessCondition.class) (2)
@Secondary (3)
public class AwsSesMailService implements EmailService {
private static final Logger LOG = LoggerFactory.getLogger(AwsSesMailService.class);
protected final String sourceEmail;
protected final SesClient ses;
public AwsSesMailService(@Nullable @Value("${AWS_REGION}") String awsRegionEnv, (4)
@Nullable @Value("${AWS_SOURCE_EMAIL}") String sourceEmailEnv,
@Nullable @Value("${aws.region}") String awsRegionProp,
@Nullable @Value("${aws.sourceemail}") String sourceEmailProp) {
this.sourceEmail = sourceEmailEnv != null ? sourceEmailEnv : sourceEmailProp;
String awsRegion = awsRegionEnv != null ? awsRegionEnv : awsRegionProp;
this.ses = SesClient.builder().region(Region.of(awsRegion)).build();
}
@Override
public void send(@NonNull @NotNull @Valid Email email) {
SendEmailRequest sendEmailRequest = SendEmailRequest.builder()
.destination(Destination.builder().toAddresses(email.getRecipient()).build())
.source(sourceEmail)
.message(Message.builder().subject(Content.builder().data(email.getSubject()).build())
.body(Body.builder().text(Content.builder().data(email.getTextBody()).build()).build()).build())
.build();
SendEmailResponse response =ses.sendEmail(sendEmailRequest);
LOG.info("Sent email with id: {}", response.messageId());
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | Bean will not loaded unless condition is met. |
3 | In case of multiple possible interface implementations of EmailService , @Secondary reduces the priority. |
4 | Values for region and source email are resolved from environment variables or system properties. |
We annotated the previous class with @Requires(condition = AwsResourceAccessCondition.class)
.
The AwsResourceAccessCondition
ensures the bean is not loaded unless certain conditions are fulfilled.
If your application creates an AWS client using the create method, the client searches for credentials using the default credentials provider chain, in the following order:
In the Java system properties:
aws.accessKeyId
andaws.secretAccessKey
.In system environment variables:
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
.In the default credentials file (the location of this file varies by platform).
n the Amazon ECS environment variable:
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
.In the instance profile credentials, which exist within the instance metadata associated with the IAM role for the EC2 instance.
package example.micronaut;
import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;
import io.micronaut.context.env.Environment;
import io.micronaut.core.util.StringUtils;
/**
* @see <a href="https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/java-dg-roles.html">Configure IAM Roles for Amazon EC2</a>
*/
public class AwsResourceAccessCondition implements Condition {
@Override
public boolean matches(ConditionContext context) {
if (StringUtils.isNotEmpty(System.getProperty("aws.accessKeyId")) && StringUtils.isNotEmpty(System.getProperty("aws.secretAccessKey"))) { (1)
return true;
}
if (StringUtils.isNotEmpty(System.getenv("AWS_ACCESS_KEY_ID")) && StringUtils.isNotEmpty(System.getenv("AWS_SECRET_ACCESS_KEY"))) { (2)
return true;
}
if (StringUtils.isNotEmpty(System.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))) { (3)
return true;
}
return context != null && context.getBean(Environment.class).getActiveNames().contains(Environment.AMAZON_EC2); (4)
}
}
Add a test to verify the service is loaded:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
@DisabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*")
class AwsSesMailServiceTest {
@Test
public void awsSesMailServiceIsNotLoadedIfSystemPropertyIsNotPresent() {
ApplicationContext ctx = ApplicationContext.run();
assertFalse(ctx.containsBean(AwsSesMailService.class));
ctx.close();
}
@Test
public void awsSesMailServiceIsLoadedIfSystemPropertiesArePresent() {
String accesskeyid = System.getProperty("aws.accessKeyId");
String awssecretkey = System.getProperty("aws.secretAccessKey");
String awsregion = System.getProperty("aws.region");
String sourceemail = System.getProperty("aws.sourceemail");
System.setProperty("aws.accessKeyId", "XXXX");
System.setProperty("aws.secretAccessKey", "YKYY");
System.setProperty("aws.region", "XXXX");
System.setProperty("aws.sourceemail", "me@micronaut.example");
ApplicationContext ctx = ApplicationContext.run();
AwsSesMailService bean = ctx.getBean(AwsSesMailService.class);
assertEquals("me@micronaut.example", bean.sourceEmail);
ctx.close();
if (awsregion == null) {
System.clearProperty("aws.region");
} else {
System.setProperty("aws.region", awsregion);
}
if (sourceemail == null) {
System.clearProperty("aws.sourceemail");
} else {
System.setProperty("aws.sourceemail", sourceemail);
}
if (accesskeyid == null) {
System.clearProperty("aws.accessKeyId");
} else {
System.setProperty("aws.accessKeyId", accesskeyid);
}
if (awssecretkey == null) {
System.clearProperty("aws.secretAccessKey");
} else {
System.setProperty("aws.secretAccessKey", awssecretkey);
}
}
}
4.3.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:
implementation("com.sendgrid:sendgrid-java:4.8.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.
package example.micronaut;
import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;
import io.micronaut.core.util.StringUtils;
public class SendGridEmailCondition implements Condition {
@Override
public boolean matches(ConditionContext context) {
return envOrSystemProperty("SENDGRID_APIKEY", "sendgrid.apikey") &&
envOrSystemProperty("SENDGRID_FROM_EMAIL", "sendgrid.fromemail");
}
private boolean envOrSystemProperty(String env, String prop) {
return StringUtils.isNotEmpty(System.getProperty(prop)) || StringUtils.isNotEmpty(System.getenv(env));
}
}
Add a test:
package example.micronaut;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SendGridEmailConditionTest {
@Test
public void conditionIsTrueIfSystemPropertiesArePresent() {
String sendGridApiKey = System.getProperty("sendgrid.apikey");
String sendGrindFromEmail = System.getProperty("sendgrid.fromemail");
System.setProperty("sendgrid.apikey", "XXXX");
System.setProperty("sendgrid.fromemail", "me@micronaut.example");
SendGridEmailCondition condition = new SendGridEmailCondition();
assertTrue(condition.matches(null));
if (sendGridApiKey == null) {
System.clearProperty("sendgrid.apikey");
} else {
System.setProperty("sendgrid.apikey", sendGridApiKey);
}
if (sendGrindFromEmail == null) {
System.clearProperty("sendgrid.fromemail");
} else {
System.setProperty("sendgrid.fromemail", sendGrindFromEmail);
}
}
@Test
public void conditionIsFalseIfSystemPropertiesAreNotPresent() {
SendGridEmailCondition condition = new SendGridEmailCondition();
assertFalse(condition.matches(null));
}
}
package example.micronaut;
import com.sendgrid.SendGrid;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.Method;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Personalization;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.IOException;
@Singleton (1)
@Requires(condition = SendGridEmailCondition.class) (2)
class SendGridEmailService implements EmailService {
private static final Logger LOG = LoggerFactory.getLogger(SendGridEmailService.class);
protected final String apiKey;
protected final 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.getTextBody() !=null ) {
return new Content("text/plain", email.getTextBody());
}
if ( email.getHtmlBody() !=null ) {
return new Content("text/html", email.getHtmlBody());
}
return null;
}
@Override
public void send(@NonNull @NotNull @Valid Email email) {
Personalization personalization = new Personalization();
personalization.setSubject(email.getSubject());
com.sendgrid.helpers.mail.objects.Email to = new com.sendgrid.helpers.mail.objects.Email(email.getRecipient());
personalization.addTo(to);
if ( email.getCc() != null ) {
for ( String cc : email.getCc() ) {
com.sendgrid.helpers.mail.objects.Email ccEmail = new com.sendgrid.helpers.mail.objects.Email();
ccEmail.setEmail(cc);
personalization.addCc(ccEmail);
}
}
if ( email.getBcc() != null ) {
for ( String bcc : email.getBcc() ) {
com.sendgrid.helpers.mail.objects.Email bccEmail = new com.sendgrid.helpers.mail.objects.Email();
bccEmail.setEmail(bcc);
personalization.addBcc(bccEmail);
}
}
Mail mail = new Mail();
com.sendgrid.helpers.mail.objects.Email from = new com.sendgrid.helpers.mail.objects.Email();
from.setEmail(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.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sg.api(request);
if (LOG.isInfoEnabled()) {
LOG.info("Status Code: {}", String.valueOf(response.getStatusCode()));
LOG.info("Body: {}", response.getBody());
LOG.info("Headers {}", response.getHeaders()
.keySet()
.stream()
.map(key -> key.toString() + "=" + response.getHeaders().get(key))
.collect(Collectors.joining(", ", "{", "}")));
}
} catch (IOException ex) {
LOG.error(ex.getMessage());
}
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | Bean will not loaded unless condition is met. |
3 | Values will be resolved from system properties. |
Add a test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class SendGridEmailServiceTest {
@Test
public void sendGridEmailServiceIsNotLoadedIfSystemPropertyIsNotPresent() {
ApplicationContext ctx = ApplicationContext.run();
assertFalse(ctx.containsBean(SendGridEmailService.class));
ctx.close();
}
@Test
public void sendGridEmailServiceIsLoadedIfSystemPropertiesArePresent() {
String sendGridApiKey = System.getProperty("sendgrid.apikey");
String sendGrindFromEmail = System.getProperty("sendgrid.fromemail");
System.setProperty("sendgrid.apikey", "XXXX");
System.setProperty("sendgrid.fromemail", "me@micronaut.example");
ApplicationContext ctx = ApplicationContext.run();
SendGridEmailService bean = ctx.getBean(SendGridEmailService.class);
assertEquals("XXXX", bean.apiKey);
assertEquals("me@micronaut.example", bean.fromEmail);
ctx.close();
if (sendGridApiKey == null) {
System.clearProperty("sendgrid.apikey");
} else {
System.setProperty("sendgrid.apikey", sendGridApiKey);
}
if (sendGrindFromEmail == null) {
System.clearProperty("sendgrid.fromemail");
} else {
System.setProperty("sendgrid.fromemail", sendGrindFromEmail);
}
}
}
4.4. Run the application
Add a logger to get more visibility:
<logger name="example.micronaut" level="TRACE"/>
To use SendGrid, define the required environment variables and run the application.
$ export SENDGRID_FROM_EMAIL=email@email.com
$ export SENDGRID_APIKEY=XXXXXX
./gradlew run
To use AWS SES, define the required environment variables and run the application.
$ 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 SendGrid EmailService
implementation will be used due to the @Secondary
annotation in AwsSesMailService
.
curl -X "POST" "http://localhost:8080/mail/send" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"subject": "Test Email",
"recipient": "recipient@email.com",
"textBody": "Foo"
}'
4.5. Test
In our acceptance test, beans SendGridEmailService
or AwsSesMailService
will not be loaded since system properties are not present.
Instead, we set up a Mock which we can verify interactions against.
package example.micronaut;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Primary
@Requires(property = "spec.name", value = "mailcontroller")
@Singleton
public class MockEmailService implements EmailService {
public List<Email> emails = new ArrayList<>();
@Override
public void send(@NonNull @NotNull @Valid Email email) {
emails.add(email);
}
}
Create the next test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import java.util.Collection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@MicronautTest (1)
@Property(name = "spec.name", value = "mailcontroller") (2)
class MailControllerTest {
@Inject
ApplicationContext applicationContext; (3)
@Inject
@Client("/")
HttpClient client; (4)
@Test
public void mailsendInteractsOnceEmailService() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Test");
cmd.setRecipient("delamos@grails.example");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (5)
EmailService emailService = applicationContext.getBean(EmailService.class);
assertTrue(emailService instanceof MockEmailService);
int oldEmailsSize = ((MockEmailService) emailService).emails.size();
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
assertEquals(oldEmailsSize + 1 , ((MockEmailService) emailService).emails.size()); (6)
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
2 | Annotate the class with @Property to supply configuration to the test. |
3 | Inject the ApplicationContext bean |
4 | Inject the HttpClient bean and point it to the embedded server. |
5 | Creating HTTP Requests is easy thanks to the Micronaut framework fluid API. |
6 | emailService.send method is invoked once. |
4.6. Validation
We want to ensure any email request contains a subject, recipient and a text body or html body.
Micronaut validation is built on the standard framework – JSR 380, also known as Bean Validation 2.0.
Hibernate Validator is a reference implementation of the validation API. Micronaut has built-in support for validation of beans that are annotated with javax.validation
annotations.
The necessary dependencies are included by default when creating a new application, so you don’t need to add anything else.
Create the next test:
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import jakarta.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest (1)
@Property(name = "spec.name", value = "mailcontroller") (2)
class MailControllerValidationTest {
@Inject
ApplicationContext applicationContext;
@Inject
@Client("/")
HttpClient client;
@Test
public void mailSendCannotBeInvokedWithoubSubject() {
EmailCmd cmd = new EmailCmd();
cmd.setRecipient("delamos@micronaut.example");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCannotBeInvokedWithoutRecipient() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setTextBody("Hola hola");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCannotBeInvokedWithoutEitherTextBodyOrHtmlBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
Executable e = () -> client.toBlocking().exchange(request);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.BAD_REQUEST, thrown.getStatus());
}
@Test
public void mailSendCanBeInvokedWithoutTextBodyAndNotHtmlBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
cmd.setTextBody("Hello");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
}
@Test
public void mailSendCanBeInvokedWithoutHtmlBodyAndNotTextBody() {
EmailCmd cmd = new EmailCmd();
cmd.setSubject("Hola");
cmd.setRecipient("delamos@micronaut.example");
cmd.setHtmlBody("<h1>Hello</h1>");
HttpRequest<EmailCmd> request = HttpRequest.POST("/mail/send", cmd); (3)
HttpResponse<?> rsp = client.toBlocking().exchange(request);
assertEquals(HttpStatus.OK, rsp.getStatus());
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
2 | Define a property available for the application. |
3 | Creating HTTP Requests is easy thanks to the Micronaut framework fluid API. |
In order to satisfy the test, create an email constraints annotation
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 = {})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailConstraints {
String message() default "{email.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and a constraint validator in a @Factory
class:
package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.util.StringUtils;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import jakarta.inject.Singleton;
@Factory
public class EmailConstraintsFactory {
@Singleton
ConstraintValidator<EmailConstraints, EmailCmd> emailBodyValidator() {
return (value, annotationMetadata, context) ->
value != null &&
(StringUtils.isNotEmpty(value.getTextBody()) || StringUtils.isNotEmpty(value.getHtmlBody()));
}
}
Annotate EmailCmd
with EmailConstraints
and @Introspected
(to generate the
Bean Introspection information).
@EmailConstraints
@Introspected
public class EmailCmd implements Email {
@NotNull
@NotBlank
private String recipient;
@NotNull
@NotBlank
private String subject;
private List<String> cc = new ArrayList<>();
private List<String> bcc = new ArrayList<>();
private String htmlBody;
private String textBody;
private String replyTo;
// getters & setters
}
5. Testing the Application
To run the tests:
./gradlew test
Then open build/reports/tests/test/index.html
in a browser to see the results.
6. Next steps
Explore more features with Micronaut Guides.
7. Help with the Micronaut Framework
Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.