Test Email Applications with Serenity BDD and JBehave
Simplify email acceptance testing with MailSlurp and Serenity framework by testing real applications with real email addresses.
Serenity is a very popular behaviour driven development testing framework. It enables QA testers and developers write narrative style stories to describe user behaviour in applications and implement test logic to perform automated tests against real application.
Full example code available on Github.
By combining Serenity and MailSlurp you can create test email accounts in code and test your application end-to-end using real emails. In this example we will use Java and Selenium to test a demonstration web app hosted at playground.mailslurp.com.
The application has a typical user sign up form that accepts an email address and password. Upon submission a confirmation code is emailed to the user containing a verification code. This code must be submitted to the page to confirm the user account, after which the user may log in and see a picture of a friendly dog.
Setup test project
We can create a Serenity test project with Java and JBehave using the mvn archetype:generate -Dfilter=serenity
command and then selecting the Jbehave Selenium option. This should generate a folder with a structure like so:
├── pom.xml
├── README.md
├── serenity.properties
└── src
└── test
├── java
│ └── com
└── resources
└── stories
Edit the pom.xml
and add the MailSlurp dependency. This will allow us to create test email accounts in code. MailSlurp is free for personal use but you need an API KEY. Sign up to use the library.
<dependency>
<groupId>com.mailslurp</groupId>
<artifactId>mailslurp-client-java</artifactId>
<version>11.5.11</version>
</dependency>
Describe our test narrative
Inside the stories
folder create a new directory called sign_up_user
. Inside that folder add a narative.txt
to describe the test:
Sign up a user
To sign up for a web application
As a user
I want to be able to sign up for an application using my email address, verify the address, and log in
This is always a good step to prepare for what we hope to achieve with our tests.
Creating a story
Serenity uses a behaviour driven approach meaning we model our tests based on user behavior. Each test is called a story. A story is written in a .story
file and the statements within this file map to functions we define in Java later.
We want to test the sign up, confirmation, and login process for our demo app. Let's create a SignUpUser.story
file and enter a story:
Sign Up User
Narrative:
In order to use an authenticated application
As a user with an email address
I want to sign up to an application and verify my acount
Scenario: Sign Up User
Given the user has email address and is on the example application page
When the user signs up with an email address and password 'test-password'
Then they receive a confirmation code, confirm their account, login with 'test-password' and see 'Welcome'
The scenario describes what we expect to happen given an initial condition and a user action. The next step is to write Java methods that map to each statement in our scenario. Let's start.
Writing Java bindings
Create a package inside your src/test
folder. We will name ours com.mailslurp.examples
. Next create a root AcceptanceTestSuite
class:
Configure the test suite
package com.mailslurp.examples;
import net.serenitybdd.jbehave.SerenityStories;
public class AcceptanceTestSuite extends SerenityStories {}
This will enable our tests to run with mvn test
.
Add MailSlurp client
Next we need to set up a MailSlurp client to send and receive emails. This looks like so:
package com.mailslurp.examples;
import com.mailslurp.clients.ApiClient;
import com.mailslurp.clients.Configuration;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
public class MailSlurpClient {
// set your MailSlurp API KEY via environment variable
private static final String apiKey = System.getenv("API_KEY");
// set a timeout so we can wait for emails to arrive
public static final Long TIMEOUT = 30000L;
private final ApiClient apiClient;
public MailSlurpClient() {
assertThat(apiKey, notNullValue());
// create a MailSlurp client and http client
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.build();
apiClient = Configuration.getDefaultApiClient();
// IMPORTANT set api client timeouts
apiClient.setConnectTimeout(TIMEOUT.intValue());
apiClient.setWriteTimeout(TIMEOUT.intValue());
apiClient.setReadTimeout(TIMEOUT.intValue());
// IMPORTANT set API KEY and client
apiClient.setHttpClient(httpClient);
apiClient.setApiKey(apiKey);
}
public ApiClient getClient() {
return apiClient;
}
}
Create a PageObject for our webapp
Next we need a way for Selenium to load and query our webapp. Create a pages
folder and add a PlaygroundApplication
class.
package com.mailslurp.examples.pages;
import net.thucydides.core.annotations.DefaultUrl;
import net.thucydides.core.pages.PageObject;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
@DefaultUrl("https://playground.mailslurp.com")
public class PlaygroundApplication extends PageObject {
public void sign_up_with_email_and_password(String emailAddress, String password) {
// click sign up button
find("//a[@data-test='sign-in-create-account-link']").click();
// expect sign up page
waitFor("//*[@data-test='sign-up-header-section']//span").shouldContainText("Sign Up");
// enter email and password
find("//*[@name='email']").type(emailAddress);
find("//*[@name='password']").type(password);
// submit sign up
find("//button[@data-test='sign-up-create-account-button']").click();
// should show confirm page
waitFor("//*[@data-test='confirm-sign-up-header-section']//span").shouldContainText("Confirm");
}
public void wait_for_page_text(String text) {
waitForCondition().until(ExpectedConditions.textToBePresentInElement(find(By.tagName("BODY")), text));
}
public void login_with_email_address_and_password(String emailAddress, String password) {
// enter email and password
find("//*[@data-test='username-input']").type(emailAddress);
find("//*[@data-test='sign-in-password-input']").type(password);
find("//button[@data-test='sign-in-sign-in-button']").click();
}
public void submit_confirmation_code(String code) {
find("//*[@name='code']").type(code);
find("//button[@data-test='confirm-sign-up-confirm-button']").click();
}
}
Each method describes a set of actions that will take place against the playground app such as submiting a login form. We will call these methods from our steps.
Writing test steps
The next stage is to write steps for our tests. This bind story statements to a series of Java methods.
Definition steps
Create a steps
directory and inside that create a DefinitionSteps
class.
package com.mailslurp.examples.steps;
import com.mailslurp.clients.ApiException;
import com.mailslurp.models.Inbox;
import net.thucydides.core.annotations.Steps;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import com.mailslurp.examples.steps.serenity.EndUserSteps;
public class DefinitionSteps {
private Inbox inbox;
@Steps
EndUserSteps endUser;
@Given("the user has email address and is on the example application page")
public void givenTheUserIsOnTheExampleApplicationPage() throws ApiException {
inbox = endUser.has_email_address();
endUser.is_on_the_application_page();
}
@When("the user signs up with an email address and password '$password'")
public void thenTheUserSignsUpWithAnEmailAddressAndPassword(String password) {
endUser.signs_up_with_email_address_and_password(inbox, password);
}
@Then("they receive a confirmation code, confirm their account, login with '$password' and see '$message'")
public void thenTheyReceiveAConfirmationCodeConfirmTheirAccountLoginAndSee(String password, String message) throws ApiException {
endUser.receive_confirmation_confirm_account_login_and_see_message(inbox, password, message);
}
}
Note how each @Given
, @When
and @Then
annotation on a method maps to the same words used in the SignUpUser.story file
. This is the magic of Serenity and allows us to write tests in plain statements in story files that map to Java steps.
End user steps
We can abstract some of the step logic into an EndUserSteps
class. Place this inside a steps/serenity
and add the following:
package com.mailslurp.examples.steps.serenity;
import com.mailslurp.examples.MailSlurpClient;
import com.mailslurp.examples.pages.PlaygroundApplication;
import com.mailslurp.models.Email;
import com.mailslurp.models.Inbox;
import net.thucydides.core.annotations.Step;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import com.mailslurp.apis.*;
import com.mailslurp.clients.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EndUserSteps {
PlaygroundApplication playgroundApplication;
MailSlurpClient mailSlurpClient = new MailSlurpClient();
@Step
public Inbox has_email_address() throws ApiException {
// create an inbox controller to create a real email address
InboxControllerApi inboxControllerApi = new InboxControllerApi(mailSlurpClient.getClient());
// create an email address for the test user
Inbox inbox = inboxControllerApi.createInbox(null,null,null,null,null,null,null,null,null);
assertThat(inbox.getEmailAddress(), containsString("@mailslurp."));
return inbox;
}
@Step
public void is_on_the_application_page() {
playgroundApplication.open();
}
@Step
public void receive_confirmation_confirm_account_login_and_see_message(Inbox inbox, String password, String message) throws ApiException {
// fetch the latest email for the inbox
WaitForControllerApi waitForControllerApi= new WaitForControllerApi(mailSlurpClient.getClient());
Email email = waitForControllerApi.waitForLatestEmail(inbox.getId(), MailSlurpClient.TIMEOUT, true);
// extract the code from the email
Pattern p = Pattern.compile("Your Demo verification code is ([0-9]{6})");
Matcher m = p.matcher(email.getBody());
m.find();
String code = m.group(1);
// submit the code
playgroundApplication.submit_confirmation_code(code);
// now login
playgroundApplication.login_with_email_address_and_password(inbox.getEmailAddress(), password);
playgroundApplication.wait_for_page_text(message);
}
@Step
public void signs_up_with_email_address_and_password(Inbox inbox, String password) {
playgroundApplication.sign_up_with_email_and_password(inbox.getEmailAddress(), password);
}
}
Run the tests
We now have a story and steps associated with it. Make sure you have the Firefox Geckodriver installed so that Selenium can open Firefox and execute our tests. Next run API_KEY=your-mailslurp-api-key mvn test
.
If successful the test will create a new email address, sign up a user, receive the confirmation code, extract the verification, submit it and login. Then a dog will be shown to welcome us.
How does it work?
That was a log of code. Let's review some important parts.
Creating inboxes
You can use MailSlurp to create test email accounts on demand.
@Step
public Inbox has_email_address() throws ApiException {
// create an inbox controller to create a real email address
InboxControllerApi inboxControllerApi = new InboxControllerApi(mailSlurpClient.getClient());
// create an email address for the test user
Inbox inbox = inboxControllerApi.createInbox(null,null,null,null,null,null,null,null,null);
assertThat(inbox.getEmailAddress(), containsString("@mailslurp."));
return inbox;
}
Receiving emails in tests
Once the user has signed up we can then ues MailSlurp to wait for the email to arrive.
WaitForControllerApi waitForControllerApi= new WaitForControllerApi(mailSlurpClient.getClient());
Email email = waitForControllerApi.waitForLatestEmail(inbox.getId(), MailSlurpClient.TIMEOUT, true);
Then we can use a regex pattern to extract the verification code from the email body:
// extract the code from the email
Pattern p = Pattern.compile("Your Demo verification code is ([0-9]{6})");
Matcher m = p.matcher(email.getBody());
m.find();
String code = m.group(1);
// submit the code
playgroundApplication.submit_confirmation_code(code);
Conclusion
Serenity is a powerful BDD testing framework. With JBehave you can map stories to Java methods. By adding MailSlurp you can create real email addresses and use them to test user sign up, authentication, newsletters and more. See docs.mailslurp.com for more information and examples.