Twilio SendGrid sends a lot of emails. To send all of our transactional emails—ranging from password reset to account email verification to export CSV emails—we use our own backend services. 

We recently passed a huge milestone of over 3 trillion emails sent.

In our testing environments, we send our emails to test email inboxes in self-hosted Squirrelmail servers to avoid sending out test emails to actual email inbox service providers such as Gmail. Many important flows require the user to check their email, click on an actionable link, redirect back to the web application, and then continue onward at some download or verification success page. 

We test these features manually by inputting our Squirrelmail email addresses in the necessary forms, clicking some buttons, and following email links to validate things work as expected. We can do this every time upon new code changes to ensure we did not regress anywhere, but it would be nice to automate these steps in an end-to-end (E2E) test we can run again whenever we want to. Specifically, we would like to write E2E tests with Cypress, so we do not have to test these potentially slow and confusing email flows manually in our own web browser every time. 

Before we get into the post, here are a couple of articles you may be interested in reading first.

  • If you have never written E2E tests before or would like a refresher about how to think when writing E2E tests, you may want to see this blog post before we start.
  • If you aren’t familiar with using Cypress for writing E2E tests in general, we strongly recommend you check out our one thousand foot overview about implementing Cypress tests for your web applications—this will give you a better idea of the Cypress API. 

This post assumes you know some of the Cypress functions such as cy.task() to run arbitrary code we define in a Node server to assist us with dealing with emails. Also, if the later code snippets with TypeScript are a bit confusing, it may clear things up to see our blog post about how we typed our Cypress tests. You can still modify the code in your own Cypress tests by removing the type definitions and sticking to JavaScript-only syntax.

We will not cover how to set up your own test email inbox server (like Squirrelmail), but we will focus on automating these steps related to searching for emails, parsing matching email contents, and following email links. This should give you a better picture of what kind of functions to use and implement to handle these email flows, assuming you have a test email inbox server and your own credentials to connect to. 

How do we deal with email flows in Cypress tests?

For us to test out the entire email flows, we built out cy.task()plugins to:

  • Deal with connecting to and filtering through email inboxes for emails with a certain subject line
  • Retrieving a matching email’s body contents
  • Deleting emails from a user’s inbox without ever having to log in through Squirrelmail UI

We also went this route because we do not own or have control over the Squirrelmail UI, and it is not possible to visit more than one superdomain in a Cypress test since the URLs for the Squirrelmail UI live in a separate superdomain from our deployed frontend app.  

We first installed a library called “emailjs-imap-clientto help us set up an IMAP client to connect to our Squirrelmail inbox through some credentials and host configurations. Using this library, we encapsulated all the Squirrelmail related things inside a module called squirrelmail.ts that we would later import in our plugins/index.ts for our cy.task() function definitions.

Before tests involving emails run, we should tear down all the emails with the same subject line to avoid false positives in accidentally referring to an older email triggered in a prior test. To handle this use case, we implemented this task to delete all emails with a matching subject line in a user’s inbox as follows.


During our tests, we trigger an action that will lead to an email being sent to the user’s Squirrelmail email address and often need to wait for the email with a matching subject line to arrive in the user’s email inbox. This process takes anywhere from seconds to minutes, depending on how involved the backend processes are. We need to make sure to poll until it arrives or provide a timeout error in the test to let us know if something is not working or delayed in the mail send part. Since we already deleted emails with matching subject lines beforehand, we can be mostly certain it was triggered from our test run if it does return successfully.

Here is how we developed the functionality for waiting for an email with a specific subject line such as “Your email activity export” or “Sender verification” to arrive in a user’s email inbox.

So far:

  • We cleared out the user’s email inbox
  • The test runs and triggers an email to be sent to the user’s email inbox
  • We successfully waited for the email to arrive in the user’s email inbox

Now, we need to get that specific email’s body contents.

Thankfully, we can return the matching email’s body contents as a string that we would later have to parse through for the action link to return to the web app we control and own. The task plugin below searches through a user’s inbox for an email with a matching subject line and returns the body contents for us to use later.

As a brief reminder, we could not simply create page objects for the Squirrelmail pages, visit Squirrelmail through the UI, filter for a matching subject line, open up the email, click on the actionable link directly, and be on our merry way back to our web app—because we cannot visit multiple superdomains in the same Cypress test. It is also more of an antipattern to visit pages and applications you do not control or own.

After finding the matching email body contents we triggered in the test, we have to parse through the HTML content, find the action link, trigger an HTTP request to the link, and then follow the redirect back to our web application.

For parsing through the email HTML contents and finding the action link parts, we utilized another library called “cheerio”, which loads up the HTML string and allows us to call jQuery-like functions to extract out the action buttons or links we need. Once we parsed out the links, we make an HTTP request to the link with cy.request(), follow the redirect link back to the web app we control and own on the one superdomain, and proceed with verifying success states on the page we redirected to.

In your case, you may not need to trigger an HTTP request to the link and follow the response’s redirect if your link already points to the proper place. If the link URL already points to your web app directly, nothing is stopping you from extracting out the link path and doing a cy.visit(linkPath) to redirect back to your app. In the case of Twilio SendGrid links, the links may look like “…sendgrid.net?…” if you have link tracking on for your emails or “brandedlink.com” if you have link branding on. That is why we would need to make an HTTP request and extract the redirect path to do a cy.visit(redirectPath) because the immediate “href” of the links do not match up with our web app.

Below is an example of finding the link with cheerio, making an HTTP request to the link, and following the redirect.

Conclusion

We walked you through the many cy.task() plugin functions we implemented to do more read and delete actions with matching emails in our inboxes. We created these functions to properly reset the user’s email inbox state before we trigger these email flows in the web pages, wait for the emails to arrive in the inbox, and finally follow the links back to their success states. We summarize the key steps for your Cypress tests below:

  • Tear down all the emails with a certain subject line to avoid false positives with cy.task(“teardownMatchingEmails”).
  • Log in to a user through the API and then work through a set of steps through the UI to generate that email to be sent to the user’s email inbox.
  • Poll for the user’s email inbox to receive the email with the matching subject line through cy.task(“awaitEmailInSquirrelmailInbox”).
  • Read the email’s body contents with the matching subject line using cy.task(“squirrelmailSearchBySubject”).
  • Parse out the proper action link with the cheerio library by passing in the email body HTML string and searching through elements with a jQuery-like syntax.
  • Make an HTTP request on the parsed out email links through cy.request(“link”) and follow the redirect response back to the web app or visit the path if the links already match up with your superdomain with cy.visit(“emailLinkToWebApp”).
  • Verify success states occur or follow up with more UI steps on the page you own.

We hope this blog post encourages you to start testing thoroughly from start to finish. We used to avoid writing E2E tests with email flows, but thankfully, we figured out a way with these Cypress tests to save us a lot of time we would have spent in manually regression testing everything. We learned it is much more valuable to automate and test the whole happy path flow rather than parts of the flow—unless many steps rely on third-party services you do not own or control or it is not possible to reset the user back to a certain state reliably.

If you are interested in more blog posts related to what we learned about writing Cypress tests for our web applications, check out the following articles:



Alfred Lucero is a software engineer for the Mako Frontend team at SendGrid. His favorite part about web development is bringing new designs and ideas to life. He spends his free time exploring Southern California and playing with his corgi, Juno.