Test automation will check your email

Giuseppe Cioce
River Island Tech
Published in
8 min readDec 3, 2019

Since I started my career as an SDET, I have struggled to find a solution for this topic: how can I automate a process that verifies the reception of an email?

Emails are crucial for all kinds of businesses, and as SDTE — especially at River Island — it is important to ensure that the customer will receive the confirmation of his order, rather than the latest offers, etc.

Well, after spending a lot of time looking on the web for something that convinced me, my first approach was using Selenium. A simple application that interacts with the web page. Even though this was working fine, it was not “simple”. It required a lot of time for setting up the project, setting up the web driver, writing the test and, most of all, maintaining it. On top of this, it was also slowing down the whole flow and it was not fitting our scope (i.e. API testing). In conclusion all this effort was not justified so we decided to remove it from our pipeline and carry out this kind of test manually.

Recently, this topic has arisen again and finally I have found and implemented a solution that works fine, fits our scope and most of all… I like! So let me share my experience with you.

This approach uses Go as a language, Gingko as BDD framework, Gomega as matching library and most importantly Google GMail API for checking emails in the inbox (you can find reference here).

I will skip the part of the configuration in Go (with Ginkgo and Gomega) as you can find loads of articles on this. I will just walk you through the configuration of the Google account on which I noticed lack of information on the web.

Configuring Google Account

You need to have a valid account in order to complete the process successfully. After you created it, you need to enable the OATH2 in order to grant the user permission to use the google API. For reference, let’s assume our account is test@gmail.com

Go to the following link https://console.developers.google.com/, and once accepted all the mandatory fields, you will be required to create a project

Click on Create and then fill the mandatory information on the next page (only ProjectName is required at this stage)

Once the project is created, back on Credentials page you will see the following screen. Select Create Credentials then from the menu select OAuth client ID

Before the system allows to select the Application type we want to use, we need to Configure consent screen . So click on the button

and fill in all the mandatory information. Then press Save

We are now able to select the type of application. For this example I will select Other and then type the Name for the Client, then the Create button

Once created, the page will show you your ClientID and the ClientSecret . You do not have to store it anywhere as this will be available here any time you need it

Cool, so now we have successfully created OAuth credentials that we will use to access the API resource.

We now need to enable the GMail API on our account. To do this, just click on Dashboard (left side menu) and, on the page, click on ENABLE API AND SERVICE

On the the page we need to select the the GMail API

and the then just click ENABLE

Well done! The configuration is now terminated and it is time to request our first access token. For the first use only, we will need to allow the application to access the account.

Request Authorisation Code

In order to request the first access token, we need to have an authorisation code to indicate that the user has given permission. Go to the following url, follow the steps and allow the application to access the account

https://accounts.google.com/o/oauth2/v2/auth?client_id={my-client_id}&response_type=code&scope=https://mail.google.com/&redirect_uri=http://localhost&access_type=offline

The most important part in the above url, is the parameter access_type=offline. This will allow the application to refresh the access tokens automatically, and this is exactly the main point: we want to run an automated test but we cannot grant the permission every time.

Once you have successfully granted the permission to the app, the resulting url should be something like this

https://oauth2.example.com/auth?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7

Store the code as this will be mandatory to request our first access token, and we will use it in a moment. After this, you will not need it anymore because when the token will expire the application will use the refresh token to negotiate a new token. I suggest a read through the official documentation that will sort any doubts on this part.

Code

As I said, I will skip all the part of Go configuration sdk, Ginkgo and Gomega as it does not really matter what language or technologies you choose (the first two parts of this post would be the same), and just jump into the topic.

After you have set up your project, get the Gmail API Go client library and OAuth2 package using the following commands

go get -u google.golang.org/api/gmail/v1
go get -u golang.org/x/oauth2/google

Once you have successfully installed the dependency, create a credential.json file containing all the information about the configuration of the account we made

{
"Config": {
"ClientID" : {{my-client-id}},
"ClientSecret": {{my-client-secret}},
"RedirectURL": "http://localhost",
"Scopes": ["https://mail.google.com/"],
"Endpoint": {
"AuthURL": "https://accounts.google.com/o/oauth2/auth",
"TokenURL": "https://www.googleapis.com/oauth2/v4/token"
}
},
"Code": {{this-is-the-code-you-have-received-in-the-url-page-after-we-have-grated-successfully-the-permission-to-the-app-to-access-the account}}
}

Now, we can create our gmclient.go that we will use to negotiate the access token and invoke the GMail API

package mail

import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
"io/ioutil"
"log"
"net/http"
"os"
)

const (
credPath = "path/to/credential.json"
tokFile = "path/to/gm_auth0_token.json"
)

type Credential struct {
oauth2.Config `json:"Config"`
Code string `json:"Code"`
}

type GMClient struct {
*gmail.Service
}

func NewGMClient() *GMClient {
c := getClient()
srv, err := gmail.NewService(context.Background(), option.WithHTTPClient(c))
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}
return &GMClient{srv}
}

// Retrieve a token, saves the token, then returns the generated client.
func getClient() *http.Client {
auth := loadCredential()

tok, err := tokenFromFile(tokFile)
if err != nil {
tok = getTokenFromWeb(&auth)
saveToken(tokFile, tok)
}

return auth.Client(context.Background(), tok)
}
// Load the
func loadCredential() Credential {
var c Credential
b, err := ioutil.ReadFile(credPath)
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}

err = json.Unmarshal(b, &c)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}

return c
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(cred *Credential) *oauth2.Token {
if cred.Code == "" {
authURL := cred.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
log.Fatalf("Go to the following link in your browser then type the "+
"authorization code (Code) in %s: \n%v\n", credPath, authURL)
}

tok, err := cred.Exchange(context.TODO(), cred.Code)

if err != nil {
log.Fatalf("Unable to retrieve token from web: %v", err)
}
return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
tok := &oauth2.Token{}
err = json.NewDecoder(f).Decode(tok)

return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
fmt.Printf("Saving credential file to: %s\n", path)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Unable to cache oauth token: %v", err)
}
defer f.Close()
_ = json.NewEncoder(f).Encode(token)
}

Note that after the first time we have requested the access token, we will store it in a file gm_auth0_toke.json and retrieve it from there the following times. This approach is required in order to not have to request a new access token in case the old one is still valid (before it expires). If it is already expired the server will negotiate a new one using the refresh_token. This approach will also work fine in case you run tests in parallel, so that all the nodes will share the same token avoiding to waste it. I took inspiration from the official doc, so have a look for the complete example.

So finally now we can create our check_email_test.go that will check if the user test@gmail.com has received an email after a certain event

package tests

import (
"mail"
. "github.com/onsi/ginkgo" //nolint: golint
. "github.com/onsi/gomega" //nolint: golint
)

var _ = Describe("check email flow", func() {

When("event is generated for user", func() {
// put here the code that will trigger the email to check

....

It("user receive a confirmation email", func() {
gmc := mail.NewGMClient()
l, err := gmc.Users.Messages.List(userLogin).Do()
Ω(err).NotTo(HaveOccurred(), "error listing messages for user %s", userLogin)
Ω(len(l.Messages)).Should(BeNumerically(">", 0), "no inbox message for user %s", userLogin)

g, err := gmc.Users.Messages.Get(userLogin, l.Messages[0].Id).Do()
Ω(err).NotTo(HaveOccurred(), "error retrieving id messages %s", l.Messages[0].Id)

expectedEmailBody := "Expected email"
actualEmailBody := g.Snippet
Ω(actualEmailBody).Should(ContainSubstring(expectedEmailBody), "the actual email body does not match the expected text value")

err = gmc.Users.Messages.Delete(userLogin, l.Messages[0].Id).Do()
Ω(err).NotTo(HaveOccurred(), "error deleting id messages %s", l.Messages[0].Id)
})
})
})

We have now completed our exercise and finally we have an automated test API base that checks the reception of an email!

Any comment, feedback or simply thoughts would be appreciated. I hope that this will help others that, like me, cannot sleep at night thinking about how to efficiently solve this puzzle :)

Enjoy

--

--