Test applications with real emails using Serenity BDD, JBehave and Selenium

serenity email testing

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.

login application

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.

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.

confirm

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.

test results

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 mailslurp.com/docs/ for more information and examples.