Running Selenium Tests in GitHub actions email test cover image

Run Selenium Tests in GitHub Actions – Email Testing

Welcome to the second part of the two-part series on running selenium tests with GitHub Actions. In the first article, we outlined how to get started and how to set up your repo for Actions. In this guide, we’ll outline how to run email integration tests and pass secrets to GitHub Actions.

To start… Why would you want to test emails with live email services in the first place? Can’t you simply write to a log file or standard out? Yes… and no. Writing to log files or standard out is ok at the beginning of the application lifecycle. Building out the email send feature takes a back seat to ensure the application actually works.

But let’s say your application is almost ready for its initial release. You want to test that you can even connect to an SMTP server. Or test to ensure the right message can go to the right inbox. Testing via log files or standard out starts to get a bit limiting in that regard. Additionally, say you want to be really sure that the contents of an email you send are what you expect them to be. At this point, you’d like to simulate as much of the email delivery as possible.

You can accomplish this with a disposable email service like Mailsac.

Allow us to plug our mail service

At Mailsac we focus on the developer experience around email automation and testing. That’s why we’ve made it so you can test out the API mentioned in this guide with a free account. You can sign up here.

So let’s lay out our testing goal:

  1. Simulate an email send with our sample application.
    1. Refer to our Guide To Stress Free Email Testing with Next.js for more information on how we created this sample application
    2. Use Selenium to drive the form and hit send
  2. Use our capture service API to send the email
  3. Use Mailsac’s API to read the email from the destination inbox
  4. Verify the contents of said email
  5. Do all this on GitHub Actions

Let’s get started.

API Credentials

If you don’t already have one, go ahead and create a mailsac account and generate an API key.

Plug those API keys in a file called .env at the root of the project:

MAILSAC_USERNAME=$MAILSAC_USERNAME
MAILSAC_API_KEY=$MAILSAC_GENERATED_KEY
MAILSAC_HOST=capture.mailsac.com
MAILSAC_PORT=5587

.env

Craft the Email

Let’s craft the email by driving Selenium through the form. Start by crafting a selenium test:

const chrome = require('selenium-webdriver/chrome');
const {Builder, Browser, By } = require('selenium-webdriver');

const screen = {
  width: 1920,
  height: 1080
};

(async function emailSendTest() {
    
  let driver = await new Builder()
    .forBrowser(Browser.CHROME)
    // .setChromeOptions(new chrome.Options().headless().windowSize(screen))
    .build();

  try {
    await driver.get('<http://localhost:3000>');
    let didSendButtonRender = await driver.findElement(By.id('sendbutton')).isDisplayed()
    
    if (!didSendButtonRender){
      throw new Error(`Send button was not rendered properly.`);
    }
 
    await driver.findElement(By.id('email')).sendKeys("[email protected]");
    await driver.findElement(By.id('comment')).sendKeys("This is some text from our Selenium test.");
    await driver.findElement(By.id('sendbutton')).click();

  } finally {
    await driver.quit();
  }
})();

tests/email-send.js

Note: I left the headless option off for this first test. You’ll want to turn the headless option back on for the test run via our continuous integration environment.

You can do a local test by running

node tests/email-send.js

And checking the inbox we tested ([email protected]) manually:

Success! Though that’s part of the way there. Let’s assert that the email contents match via the API.

Read the Email via Mailsac’s API

Thankfully, Mailsac has not only a robust API but also lots of sample code inside the docs. We’ll lift the sample email read from here:

https://docs.mailsac.com/en/latest/services/reading_mail/reading_mail.html

Note that you’ll need to install a couple of dev packages to get this to work: dotenv and superagent. dotenv is needed in this instance since our tests don’t load the entire next framework and as such, we need a method to read your .env file. superagent is a small client-side HTTP request library for doing quick HTTP calls like the one we’re about to do.

So go ahead and add them to your developer dependencies:

npm install --save-dev dotenv superagent

And add our own text comparison to Mailsac’s sample code:

require('dotenv').config()

const superagent = require('superagent')
const mailsac_api_key = process.env.MAILSAC_API_KEY
const expected_message = 'This is some text from our Selenium test.'

superagent
  .get('<https://mailsac.com/api/addresses/[email protected]/messages>')
  .set('Mailsac-Key', mailsac_api_key)
  .then((messages) => {
      const messageId = messages.body[0]._id
      superagent
          .get('<https://mailsac.com/api/text/[email protected]/>' + messageId)
          .set('Mailsac-Key', mailsac_api_key)
              .then((messageText) => {
                  if (messageText.text !== expected_message)  {
                    throw new Error(`Message '${messageText.text}' does not match expected text '${expected_message}'`)
                  }
                  else{
                    console.log("Message comparison passed");
                  }
              })
  })
  .catch(err => {
      console.log(err.message)
			process.exit(-1)
  })

tests/email-read.js

Running the test locally should result in a passing test:

$ node tests/email-read.js
Message comparison passed

Of course, if we’ll run these tests many times we’ll also want to ensure that we delete the email contents after our successful read. Let’s add a cleanup step to our read test:

require('dotenv').config()

const superagent = require('superagent')
const mailsac_api_key = process.env.MAILSAC_API_KEY;
const expected_message = 'This is some text from our Selenium test.';
const testInbox = '[email protected]';

superagent
.get(`https://mailsac.com/api/addresses/${testInbox}/messages`)
  .set('Mailsac-Key', mailsac_api_key)
  .then((messages) => {
      const messageId = messages.body[0]._id
      superagent
          .get(`https://mailsac.com/api/text/${testInbox}/` + messageId)
          .set('Mailsac-Key', mailsac_api_key)
              .then((messageText) => {
                  if (messageText.text !== expected_message)  {
                    throw new Error(`Message to delete '${messageText.text}' does not match expected text '${expected_message}'`)
                  }
                  else{
                    console.log("API Read Op: Message comparison passed");
                    superagent
                    .delete(`https://mailsac.com/api/addresses/${testInbox}/messages/${messageId}`)
                    .set('Mailsac-Key', mailsac_api_key)
                    .then((messageResponse) => {
                        console.log(`API Deletion Op: ${messageResponse.body.message}`)
                    })
                  }
              })
  })
  .catch(err => {
      console.log(err.message)
      process.exit(-1)
  })

tests/email-read.js

Let’s add it to a test script and our workflow YAML file:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "external-tests": "node tests/external-login.js",
    "test": "node tests/button-render.js && npm run mail-tests",
    "mail-tests": "node tests/email-send.js && node tests/email-read.js",
    "e2e-test": "start-server-and-test dev <http://localhost:3000> test"
  },

package.json

Note that we added a run mail-tests script to our end-to-end testing.

Try your end-to-end script locally to ensure it works:

End to end sample test

GitHub Action Test

Now that you have a working test on your local workstation, it’s time to push it up so GitHub Actions can start running your tests. If you haven’t already, read through the first article to catch up on GitHub Actions configuration and initialization.

As a reminder, this is our main.yml workflow file:

on: [push]

jobs:
  tests:
    runs-on: ubuntu-latest
    name: Run Selenium Tests
    steps:
    - name: Start selenoid
      uses: Xotabu4/[email protected]

    - uses: actions/[email protected]
    - run: npm ci  

    - name: Run end to end tests
      run: npm run e2e-test

    - name: Run external login test
      run: npm run external-tests

.github/workflows/main.yml

With all that said, let’s try and see if this email test sends an email on GitHub.

Do a git push and check your results on GitHub:

A gitHub test failure

Looks like a failure. On closer inspection:

GitHub specific failure with details

Ah, that’s right! We forgot to set our API keys at the GitHub level. Let’s go ahead and do that.

GitHub Actions Secrets

You can find it under Settings in your repo:

Github walkthrough image : Click settings

Then under Secrets → Actions click New repository secret

Add each secret that will be needed:

GitHub Secrets listing for your Actions.

Finally, ensure you add it to the workflow file:

- name: Run end to end tests
      run: npm run e2e-test
      env:
        MAILSAC_API_KEY:  ${{secrets.MAILSAC_API_KEY}}
        MAILSAC_USERNAME: ${{ secrets.MAILSAC_USERNAME }}
        MAILSAC_HOST:     ${{ secrets.MAILSAC_HOST }}
        MAILSAC_PORT:     ${{ secrets.MAILSAC_PORT }}

.github/workflows/main.yml

Do a git commit and git push and see the results on GitHub:

Successful email test with GitHub actions.

Success! You can check the details to ensure the API read and write options fire fired off:

Github actions success detailed view.

Conclusion

GitHub Actions is a powerful CI/CD tool and we have only scratched the surface of its capabilities with Selenium. We’ve also shown the power of email testing and how you can ensure the contents of every email are intentional.

We hope you enjoyed the guide, and hope to hear from you on our forums or follow our other content on LinkedIn.

Running Selenium Tests in GitHub actions Cover image

Run Selenium Tests in GitHub Actions

Browser automation is an invaluable tool. At a personal level, you can use it to automate repetitive tasks online. But browser automation can deliver so much more. At its best, it can run tasks with consistent results. Tasks that need lots of manual execution and complexity. Tasks like checking button placement, evaluating user login, and registration workflows. And in our modern era, we have our pick of browser automation frameworks.

We’ve covered browser automation before, but in this guide, we’ll do a deep dive into one of those frameworks. Additionally, we’ll cover using those testing frameworks alongside a specific continuous integration application.

In this guide we’ll walk through three testing scenarios using Selenium and GitHub Actions:

  • Check if a button rendered
  • Test a login workflow
  • Ensure the contents of a sent email

We’ll use our sample application to make our examples concrete, informative, and useful.

Repo URL: https://github.com/mailsac/mailsac-capture-service-example-nextjs

With all that said, let’s get started!

First, let’s talk about what you need to enable GitHub Actions.

Getting started with GitHub Actions

First, if you’d like a primer on GitHub Actions their page is a great resource. To summarize, it’s an easy way to run tasks when someone takes an “action” against your repo.

Actions can be:

  • Pull request merges
  • Commits
  • Repo pushes

GitHub takes your action and runs a task like integration tests, unit tests, etc. All you need to do is create a .github/workflows/main.yml file. Note: Running tasks is just one of the many possibilities for GitHub Actions. You can encompass entire workflows and even produce packages.

We’ll introduce more details further along the guide. For now, you can start by adding an empty file at:

.github/workflows/main.yml

The Sample Application We’ll Use

We’ll fork our sample application at the URL:

https://github.com/mailsac/mailsac-capture-service-example-nextjs

We’ll use this repo to walk through the examples mentioned in the intro. These sample scenarios are not meant to be comprehensive, of course. They’re meant to kick-start your journey with GitHub Actions.

To start, you’ll need to run the standard node installation and run the command:

npm install
npm run dev

Navigate to http://localhost:3000 and see our application in action:

Our initial application

With the preliminary steps done, on to the tests.

First Selenium Test: Check if the button rendered

Let’s start with the simplest test, a button render assertion test.

For this first section, we will:

  1. Add the node packages required to test
  2. Add our selenium test
  3. Add testing scripts to our package.json so that GitHub Actions can call our tests
  4. Add a simple Github Actions workflow file

Let’s start by adding a new branch (or you can fork the repo if you’d like):

git checkout -B cicd-selenium-app-test

Now let’s move on to our sample packages.

Packages: Selenium WebDriver & Start-Server-And-Test

We’ll need to add selenium and a start-server-and-test package. start-server-and-test starts our application. Then it calls our selenium tests so they can run against our running application.

npm install --save-dev start-server-and-test selenium-webdriver

start-server-and-test is one of our most straightforward ways to add live server testing capabilities. If you need more flexible frameworks, you may want to investigate mocha or cypress.

We’ll just focus on the core GitHub actions and selenium testing in this guide.

Selenium Test: Button Render

On to the test itself.

Let’s create a test under tests/button-render.js using selenium and headless chrome:

const chrome = require('selenium-webdriver/chrome');
const {Builder, Browser, By } = require('selenium-webdriver');

const screen = {
  width: 1920,
  height: 1080
};

(async function buttonRender() {
  let driver = await new Builder()
    .forBrowser(Browser.CHROME)
    .setChromeOptions(new chrome.Options().headless().windowSize(screen))
    .build();

  try {
    await driver.get('<http://localhost:3000>');
    let didSendButtonRender = await driver.findElement(By.id('sendbutton')).isDisplayed()
    if (!didSendButtonRender){
      throw new Error(`Send button was not rendered properly.`);
    }
 
  } finally {
    await driver.quit();
  }
})();

tests/button-render.js

You can find the full usage guide on selenium’s documentation page.

To help the test out, we’ll add an ID attribute to the Send button:

...
<button
    onClick={sendEmail}
    id="sendbutton"
    className="mt-3 w-full inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
  >
    Send
</button>
...

pages/index.js

Telling GitHub Actions How to Run Our Tests

We’ll need to add a couple of testing scripts to package.json:

...
"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "node tests/button-render.js",
    "e2e-test": "start-server-and-test dev <http://localhost:3000> test"
  },
...

package.json

Now let’s add the workflow main.yml file to the repo. This is the main file that kicks off a GitHub Action workflow. To keep things simple, we’ll execute our actions based on when a comitter pushes code. You can find the full list over at GitHub’s documentation pages.

on: [push]

jobs:
  tests:
    runs-on: ubuntu-latest
    name: Run Selenium Tests
    steps:
    - name: Start selenoid
      uses: Xotabu4/[email protected]

    - uses: actions/[email protected]
    - run: npm ci  

    - name: Run end to end tests
      run: npm run e2e-test

.github/workflows/main.yml

For our project, we’ll use solenoid-github-action, a GitHub Action that starts a selenium grid instance in a docker container. Solenoid is a golang reimplementation of Selenium. It makes it very easy to integrate with any continuous integration/deployment environment.

In the last portion of the file, the npm run e2e-test section kicks off the end-to-end test that starts our server and runs the selenium tests.

That’s it! Before you commit and push your code, try running it locally:

npm run e2e-test

The test should pass in your local environment. If it fails due to chrome driver issues you can find a full guide on browser driver installations here.

Send it over to GitHub via git push.

git push --set-upstream origin cicd-selenium-app-test

Head over to your repo on GitHub and under the Actions tab you should now see a selenium test run

A GitHub Actions workflow results page

You can drill down and see where our specific tests ran

Congrats! That completes our first successful test using selenium and GitHub actions. Let’s move on to something a bit more useful.

Second Selenium Test: Open and login into a web app

Let’s expand on our tests a bit. Since we’re aiming for simplicity in this guide, I won’t add a whole authentication workflow to our application. Instead, let’s focus on an existing website and attempt (and fail) to log in.

We’ll start by adding a new test to our repo.

Selenium Test: Login to Dev.to

Let’s bring in our external login Selenium test, but slightly modified:

const chrome = require('selenium-webdriver/chrome');
const {Builder, Browser, By, until } = require('selenium-webdriver');

const screen = {
    width: 1920,
    height: 1080
  };

(async function externalLogin() {
  let driver = await new Builder()
  .forBrowser(Browser.CHROME)
  .setChromeOptions(new chrome.Options().headless().windowSize(screen))
  .build();

  try {
    await driver.get('<https://dev.to>');
    await driver.findElement(By.linkText('Log in')).click();
    await driver.wait(until.titleContains('Welcome - DEV Community'), 3000);
    await driver.findElement(By.name('commit')).click();
    await driver.wait(until.titleIs(''), 3000);
    let errorBox = await driver.findElement(By.className('registration__error-notice'));
    await driver.wait(until.elementIsVisible(errorBox));
    let errorText = await errorBox.getText();

    if (!errorText.includes('Unable to login')){
      throw new Error(`Error text does not contain expected value "${errorText}"`);
    }

  }
  finally {
    await driver.quit();
  }
})();

tests/external-login.js

Only real changes are that we added a headless option for chrome.

Modifying Our Tests

Keeping things simple, let’s just add a new test script:

....
"test": "node tests/button-render.js",
"external-tests": "node tests/external-login.js",
...

package.json

And add a separate testing task to our workflow:

...
- name: Run external login test
      run: npm run external-tests
...

.github/workflows/main.yml

Let’s check our result on GitHub:

GitHub Action Failure example

Looks like a failure due to an incorrect title.

Let’s make the match less precise and rerun the test:

...
await driver.findElement(By.linkText('Log in')).click();
await driver.wait(until.titleContains('Welcome! - DEV Community'), 3000);
await driver.findElement(By.name('commit')).click();
...

tests/external-login.js

Let’s try another git push and..

A full successful external login test screen/

Success!

Conclusion

This was the first part of a two-part series about diving into GitHub Actions and Selenium tests. In the second part, we’ll run through:

  1. running our application in GitHub actions
  2. sending emails via our web form
  3. reading and comparing the email contents
  4. deleting the email contents afterward

All are driven by our selenium tests.

We hope you enjoyed the guide, and hope to hear from you on our forums or follow our other content on LinkedIn.

10 Best Testing Tools for Software Developers in 2022 Logo

10 Best Testing Tools For Software Developers in 2022

As a software developer, it’s crucial to have effective testing tools. They run the gamut from quick libraries to full-fledged analytic frameworks. They also range from free to paid. But which ones are the best in the testing space? In this article, we will list the top 10 best testing tools for software development teams.

But why test?

We can hear the groaning now. Testing is like exercise. We know we’re supposed to, but only so many of us do. Even fewer of us genuinely enjoy it. But testing doesn’t have to be a grind. In fact, we’re willing to bet a portion of you will enjoy it. Why bother with tests?

  • Testing can be fun – A lot of these tools are automation based. You can focus on crafting a comprehensive test as part of your feature building.
  • Testing can be done by other groups – It can create a bridge between you and, for example, the QA team. Commiserating around shared testing struggles can go a long way in building trust between teams.
  • An investment in yourself – Much like the exercise analogy, an investment in testing is an investment in yourself. Refactoring code, deployment flight checks, and all-around confidence about your changes can only be achieved by proper testing.

Now that you know some of the whys, let’s walk through the 10 best testing tools for software developers.

The 10 Best Testing Tools For Software Developers in 2022

1. Puppeteer 

Puppeteer is a favorite of the NodeJS community due to its easy integration into your existing build system. It automates form submission, UI testing, keyboard inputs, and more. The main limitation it does have surrounds browser support. As of this writing Puppeteer only supports Chrome. Firefox support is still in the experimental phase.

Puppeteer’s killer feature is that it installs the browser binary for you, making integrating it into your build system easy.

We’ve written an article on using Puppeteer to walk through a common testing scenario. It guides through testing a login screen to dev.to and ensuring entering a bad password does not allow you to log in. You can find it under Automate the Testing Pain Away with Browser Automation.

2. JMeter 

JMeter is a popular open-source tool that can test web applications, network performance, and more. It has a versatile GUI for manual testing and a CLI for automated testing. It also offers recording capabilities much like some other tools on this list. It’s a powerful tool with one enormous drawback: it can only integrate with Java applications.

3. Selenium WebDriver 

Selenium WebDriver is an open-source automation tool that can test web browsers across different platforms. It’s by far one of the more popular testing tools available around browser automation testing. It can incorporate into a variety of different continuous integration / continuous deployment tools.

Additionally, WebDriver is one of the few tools to be W3C recommended! In their words:

Major browser vendors (Mozilla, Google, Apple, Microsoft) support WebDriver and work constantly to improve the browsers and browser controlling code, which leads to a more uniform behavior across the different browsers, making your automation scripts more stable.

From Selenium’s Project Page
A successful Selenium WebDriver Test

To help you get started with WebDriver we’ve written a guide on creating your automated headless browser testing.

4. Mailsac 

Allow us to shamelessly plug ourselves for a moment, but for your email testing and capture needs, Mailsac is top-notch. We provide just-in-time subdomains and a robust API for automation-driven email testing.

Here’s a guide to using our on-demand subdomain feature to create Company-Wide Email Inboxes

A video walkthrough of using ad-hoc Developer Environments in your testing

5. Postman 

Postman is a flexible tool for managing and automating testing requests. It has an intuitive GUI and can generate scripts in various programming languages. You can also store playbooks in Postman to call later using their collection runner.

postman  test runner
Job Runner CLI Status From Postman’s Writing Tests Blog Post

Postman’s killer feature is the ability to integrate API calls onto your build system: 

postman   travis success
Job Runner CLI Status From Postman’s Writing Tests Blog Post

Give Postman a shot, it’s one of the most popular web API testing tools for a reason.

6. Selenium IDE

Selenium IDE is WebDriver’s GUI-driven sibling. It does a similar job of orchestrating browser functions but with the twist that it can “record” actions as you perform them in your browser.

Successful test suite
Successful test suite

Selenium IDE can test web applications, API endpoints, and everything else that WebDriver can do. It’s available as Firefox, Chrome, and Edge extensions.

Much like WebDriver we’ve written a quick start guide on automating the browser.

7. Chrome DevTools

The built-in Chrome DevTools offer a powerful extension that allows you to inspect and test web pages in real-time. Most other browsers offer this capability but Chrome is hard to beat. It offers:

  • Built-in Lighthouse report capable of grading the accessibility of your page
  • Artificial network throttling to simulate slow connection speeds
  • Performance measurement across pages
  • Hundreds of extra plugins

8. JUnit5 

JUnit5 is a popular open-source unit testing framework that can test Java applications. It offers the ability to have test runners for your test cases and enables you to focus on Test Driven Development.

It even has its own tag on Stack Overflow.

9. Cucumber Open

Cucumber is an open-source testing tool that can be used to test web applications and APIs. Cucumber is a rather unique one on this list in that it focuses on getting the specifications right the first time. It’s called Behavior Driven Development and it allows project managers and technical contributors to collaborate on concrete aspects of the application.

10. Firefox Developer Tools

Firefox does a great job innovating on the developer toolset. (3D View anyone?) Since it can do anything the Chrome DevTool set can, we’ll use this section to focus on specific plugin shoutouts:

User-Agent Switcher

Test your browser detection logic with this extension.

Ghostery

Ghostery is great for a variety of reasons, not least of which is to prevent distractions. It blocks a variety of trackers, ads, and generally improves page performance.

Cookie Manager

A versatile extension that can help you test all sorts of functionality in your application. Cookie Manager can help you with everyday tasks from authentication testing to session switching and inspection. You can additionally export and import cookie sets.

Conclusion

These are some of the best software testing tools available for developers. They all have their strengths and weaknesses, but they are all powerful tools that can help you improve your code quality.

If you’d like to discuss some of the tools you use for your software testing we’ll love to hear about it! Head on over to our community at forum.mailsac.com and discuss your must-have or time-saving tools.

Blog cover

Own Your Software Testing Workflow With Subdomains

When the topic of domains and email comes up most people begin and end the conversation at the top domain level. Subdomains seem to be left out of the conversation in their entirety. Are we trapped in our thinking about subdomains as mere marketing and newsletter features? Maybe it’s too difficult to use subdomains without an IT team involved. Maybe no one has brought up subdomains outside of meme-filled newsletters. Maybe you just haven’t thought about subdomains in general.

Why use subdomains in your testing suite?

Well, let’s break up that thinking. Subdomains have a lot to offer. Do you have trouble testing 10 different email features in your application? Does the thought of accidentally sending an email to thousands of users that says “Test” make you break into a cold sweat? Subdomains can help.

We’ll show you some of the possibilities of subdomains and walk through some use cases. We’ll also provide a quick 15-second walkthrough at the end that will setup up 2 new subdomains for development and testing purposes.

And we’ll do all this using Mailsac’s Zero-Config subdomain feature.

But before we show you some of the juicy scenarios, let’s do a quick rundown of what a subdomain actually is.

What’s the difference between an email domain and a subdomain?

Breaking down what a subdomain really means in an email

Subdomains are a way to slice up domains for specific functions like newsletters and blogs. The advantage of a subdomain is having a clear purpose tied to the name. Receiving emails from [email protected] and [email protected] show their intent from their name alone. Receiving emails from [email protected] and [email protected] is a lot vaguer. The former set clearly sends memes and educational nuggets. The latter could be a friendly name for our billing bots.

Subdomains = an easy way to differentiate email by function.

Alright, sorry about that. Had to make sure everyone was on the same page on subdomains. Let’s move on to 3 different subdomain scenarios.

Subdomain Use Case 1: Developers Get Their Own Email Domains

You work on a team of developers, and each of you needs to test the same features on a few different applications. Additionally, each feature has an email workflow attached to it.  The usual response to this is to have a shared inbox, for example, ti[email protected]. But the pain around that approach comes fast. Issues like:

  • Difficulty in separating out each developer’s testing scenarios
  • Having to sift through 1000 other unrelated emails while looking for that 1 workflow email is painful
  • Complex workflows are pretty difficult to track
An example of how you can pin an app to a a particular subdomain inbox.
A domain for me, a domain for you

Creating an email subdomain per developer is an effective way to isolate these emails across systems:

Remember that you don’t need to create these inboxes ahead of time. They are made on the fly and removed when they make sense for you, the person knee-deep in the application.

Also not shown above just yet: Mailsac’s unified inbox in action.

Subdomain Use Case 2: Company-Wide Domains per Environment

Example of segmenting via subdomains in your software testing suite.
Ensure emails stay in their zone

Environments for each set of applications are a pretty common scenario amongst enterprises. A sample above shows 2 applications split between 3 environments:

The upside of this approach is having predefined email subdomains for each environment. Developers, QA teams, and operations all know which environment the emails are associated with. QA testers can review the messages easily knowing which environment sent the emails. Operations and developers know which email address and domain to use as variables when configuring tests or environments. Ultimately, this saves time for all the teams involved.

Subdomain Use Case 3: Email Driven API Workflow

An example of how you can use subdomains in targeting specific inboxes in a subdomain.

An email-driven API workflow is a workflow that kicks off when an email arrives. The approach resembles the first scenario, where each developer gets their own domain. The difference is the usernames are less flexible. You pin it once to an API and use it for the long term. For example:

  • An email to [email protected] can trigger a Submit API action that can create a case in the HR management system
  • An email to [email protected] can trigger an Integration API action that can automatically create a ticketing workflow in your Incident Management system.

You can even string together a received email to a webhook using Mailsac’s webhook service. If you’d like to poll for updates instead we have websocket for close to real-time processing or the rest API for polling.

Alright, enough theory let’s do a walkthrough.

Walkthrough: Company-Wide Environments

Using the company-wide scenario we can have a working subdomain in a few seconds using Mailsac. A partner video will walk through the individual developer scenario.

You also have the option of using your own domain. This requires an external domain service provider. There are lots of guides out there on which domain registrar is the best

But let’s make this easy. Let’s use Mailsac’s Zero-Config Subdomain tool and bring up a new subdomain in 2 clicks. Note that you will need at least a Business Plan to make this scenario work. You can still enjoy the benefits of a single subdomain through the Indie Plan.

Zero-Click Subdomain

After creating a Mailsac account, navigate to “Custom Domains” from your dashboard:

Custom subdomain location in Mailsac's dashboard.

Type the name of the subdomain you’d like, in our case acme-dev and acme-test

Custom subdomain location and naming input in Mailsac's dashboard.

And that’s it! You should have 2 custom subdomains ready to use. Let’s put it them rough their paces. We’ll send out these emails from any client (I’ll use Gmail):

  1. To: [email protected]
    Subject: Email sent to Billing (DEV)
    Content: This is meant for dev
  2. To: [email protected]
    Subject: Timesheet Submission (DEV)
    Content: Sample timesheet submission
  3. To: [email protected]
    Subject: Timesheet Submission (TEST)
    Content: Sample timesheet submission
A manual approach to firing off emails to specific inboxes.
Nothing special here, just simulating a programmatic email

After submitting your set of emails, you *could* just check [email protected] and [email protected] individually…

Mailsac inbox where testing emails were directed to.
This is already looking painful…

…or you could use the Unified Inbox feature that displays all of your custom domains, subdomains, and private addresses in one convenient location:

Mailsac's unified inbox tool.
Much better

It’s just that easy!

Wrap Up

With this new superpower, you should be able to conjure up lots of different use cases for subdomains. The friction of creating and importing domains is completely taken care of for you. No need to register a domain with an external registrar, or manage an IT team to handle registration for you.

We encourage you to share your subdomain ideas and usage in our Discussion Forums.

Blog Cover

Automate the Testing Pain Away with Browser Automation

When I say “You need to test your code”, do you wince? Is it a feeling of guilt, one of “I know I should, but…”. Testing may not conjure up the sexiest of images. We as developers frequently put tests off until the end of our feature cycle. Or respond to a production bug by issuing a quick patch. Or worse, just bury our heads in the sand and pretend that we don’t have any bugs in our code at all. (Note: All code has bugs).

The reality is that testing is an incredible investment in your code’s future. Investing in tests is like an insurance policy: hedging your bets against an unknown future. An unknown future consisting of bitrot, dependency deprecations, or service API changes. Testing provides the ability to patch those unknowns through refactoring or flat-out removing stale dependencies. Testing can also buffer against those risks, providing peace of mind.

In this post, I’ll outline 3 different types of testing tools:

  • Selenium WebDriver
  • Selenium IDE
  • Puppeteer

To do an apples-to-apples comparison the testing scenario will be the same for all three tools. I’ll also model my testing after a user’s typical behavior. Behaviors like login attempts, searching, and form submissions. They also try to hit every layer of the application, from the user interface to the database.

Benefits of Testing a User Interface

Testing isn’t limited to the backend. Testing your interface can provide complete end-to-end testing scenarios such as:

  1. Repeated calls to your modal. Does the modal come back after the first call?
  2. Does your submit button produce an error if the form has an incorrect value?
  3. Does the UI load after a successful login to an empty state in your application?
  4. Does a specific result come back after a form search?

I’m going to walk through a straightforward testing scenario with three tools. Not to rank them, but to touch on the nuances of each. Some of these tools allow you to create tests through simple browsing. Others are headless, allowing you to drive through programming languages.

What’s a headless browser?

Conventional browsing involves rendering forms, buttons, and images to the user. A headless browser interacts with websites through code without displaying any controls. Headless browsing opens up possibilities that are tough to achieve with conventional browsers like:

  • Integration with your build systems
  • Consistency in testing
  • Decreasing the duration of your tests
  • Layout screen captures and comparisons

Tools of the Automated Browser Trade

Onto the good stuff: The tools and testing scenarios.

The Most Popular – Selenium WebDriver

Selenium is by far the most popular testing tool out there. It covers headless testing and both local and remote tests.

WebDriver targets as its core base Developers and QA Team members who can write code.

The Easiest To Get Started with – Selenium IDE

Selenium designed the IDE version for exploratory testing and bug replication. It’s perfect for walking through a bug with someone else or creating a recording of a bug for your ticketing system.

The NodeJS Fan Favorite – Puppeteer

Puppeteer is a favorite of the NodeJS community due to its easy integration into your existing build system. It automates form submission, UI testing, keyboard inputs, and more. It’s main limitation however is the browsers it supports. As of this writing Puppeteer only supports Chrome. Firefox support is, as of this writing, experimental.

Puppeteer’s killer feature is that it installs the browser binary for you, making the integrating into your build system easy.

Testing Scenario: A Failed Login to dev.to

Here’s our testing scenario:

  1. Load https://dev.to
  2. Click the “Log in” button
  3. Load a page with “Welcome! – DEV Community” in its title.
  4. Click on the “Continue” button
  5. Ensure an “Unable to login” banner appears on the page.

For consistency throughout the walkthrough, I’ll use:

  1. Chrome as my browser
  2. Javascript as the programming language of choice

Test Case 1 – Selenium WebDriver

Let’s begin with an empty directory and selenium package installation:

npm init tests
cd tests
npm install selenium-webdriver

Next, download a browser driver. You can find the full supported list in selenium’s code repository. You can place the binary anywhere. For this walkthrough, I’ll place it in the current project directory under the bin/ path.

Set your specific browser driver path:

export PATH=$PATH:$PWD/bin

I’ll be using this quick test setup (selenium.js):

const {Builder, Browser, By, Key, until} = require('selenium-webdriver');

(async function example() {
  let driver = await new Builder().forBrowser(Browser.CHROME).build();
  try {
    await driver.get('http://dev.to');
    await driver.findElement(By.linkText('Log in')).click();
    await driver.wait(until.titleIs('Welcome! - DEV Community'), 3000);
    await driver.findElement(By.name('commit')).click();
    await driver.wait(until.titleIs(''), 3000);
    let errorBox = await driver.findElement(By.className('registration__error-notice'));
    await driver.wait(until.elementIsVisible(errorBox));
    let errorText = await errorBox.getText();

    if (!errorText.includes('Error')){
      throw new Error(`Error text does not contain expected value: ${errorText}`);
    }

  } finally {
    await driver.quit();
  }
})();

Set your driver and run the file

SELENIUM_BROWSER=chrome node selenium.js
A failed Selenium Test

In general I like to ensure my tests fail from the start, followed by working towards passing the tests:

const {Builder, Browser, By, Key, until} = require('selenium-webdriver');

(async function example() {
  let driver = await new Builder().forBrowser(Browser.CHROME).build();
  try {
    await driver.get('http://dev.to');
    await driver.findElement(By.linkText('Log in')).click();
    await driver.wait(until.titleIs('Welcome! - DEV Community'), 3000);
    await driver.findElement(By.name('commit')).click();
    await driver.wait(until.titleIs(''), 3000);
    let errorBox = await driver.findElement(By.className('registration__error-notice'));
    await driver.wait(until.elementIsVisible(errorBox));
    let errorText = await errorBox.getText();

    if (!errorText.includes('Unable to login')){
      throw new Error(`Error text does not contain expected value "${errorText}"`);
    }

  } catch(e) {
    console.error(`Error running test suite: ${e.message}`)
  }
  finally {
    await driver.quit();
  }
})();

With line 15 fixed, rerun the script:

A successful Selenium Test

Success!

The above was a taste of what you can do with Selenium. You can even break out of the testing mindset and use Selenium for scraping and populating activity trackers.

On to the next tool.

Test Case 2 – Selenium IDE

While the previous test requires some programming ability, Selenium IDE is friendly to anyone who can drive a browser. The IDE version’s main use case is bug discovery, recording and profiling.

First, download the package from the Selenium page.

After installing the plugin, start a new project:

Selenium IDE introduction screen.
New Project Page
Selenium IDE's project base URL, where all of your tests will start.
Set our base url to dev.to

After you hit “Start Recording”, Selenium will launch a new Chrome window and redirect you to dev.to

Initial Dev.To Walkthrough with Selenium IDE

From the video, we:

  1. Let the initial dev.to page load
  2. Clicked on the “Log in” button
  3. Clicked on the Selenium IDE extension
  4. Stopped the Extension recording
  5. Arrived at the Commands window below
Selenium IDE's properties loaded automatically
Selenium testing properties loaded automatically

To continue our test scenario, let’s ensure that the page title is Welcome! - DEV Community and that our login attempt fails with an empty submission.

Again, I always like to have my tests fail first, so let’s start with that case. Use Selenium’s assert title command to ensure the title is what we expect. Add it to the command list:

Selenium IDE's asserting that the title fails.
Asserting the page title to fail

If you run the test, it should fail:

Failed automation testing in Selenium IDE.
Example of a failed test

Let’s go ahead and fix it with the correct title and rerun the test:

Successful Title Check

And success! Now let’s add the login check:

Walking through a complete test to success.

To summarize the video, we:

  1. Started a new recording
  2. Hit the Log in button
  3. Clicked Continue without supplying credentials
  4. Used the Selenium element picker to pick out the element we were interested in asserting.

The Commands window should now look like this:

Successful automation testing in Selenium IDE.
Added Command Check

Success!

The IDE version is the simplest to get started with and I recommend it for initial test write-ups. It can help you identify which elements you need to test against, think about app flow and what counts as a failure.

One question remains: Rendering the browser is nice, but I want to hook this into my continuous integration system. How can I do that when every test wants to load an application that requires a windowing system?

The answer is to go headless.

Test Case 3 – Puppeteer

Puppeteer is the perfect match to test web UI components inside a continuous integration system. It’s fast, headless, brings its own dependencies and runs the latest versions of Chrome and Firefox.

Let’s start by installing puppeteer on a new project:

mkdir tests
npm i puppeteer

Keep in mind that this automatically installs the chrome driver we had to manually download in the Selenium example. From Puppeteer’s documents:

When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API (customizable through Environment Variables).

https://pptr.dev/#installation

With that said, let’s create a test file that will run (and fail) our test scenario (puppeteer.js):

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  const loginSelector = 'a[href="/enter"]';
  const submitLoginSelector = '[name="commit"]';
  const errorBoxSelector = '.bad notice';
  try {
    await page.goto('https://dev.to');
    await page.waitForSelector(loginSelector,{ timeout: 3000 });
    await page.click(loginSelector);
    const pageTitle = await page.title();

    if (pageTitle !== 'Welcome! - DEV Community'){
      throw new Error(`Page title ${pageTitle} does not match expected value`);
    }
    await page.waitForSelector(submitLoginSelector,{ timeout: 3000 });
    await page.click(submitLoginSelector);
    await page.waitForSelector(errorBoxSelector,{ timeout: 3000 });
    
  }catch(e){
    console.error(`Error in test suite: ${e.message}`)
  }finally {
    await browser.close();
  }
})();
Failed test due to incorrect error box selection

Some notes on the above code:

  • Lines 6-8 are Puppeteer’s method of selecting elements on the page.
  • Like Selenium WebDriver, you have to manually check a page’s attributes and decide on what to do should they fail
    • In the above code it’s line 15, asserting the title matches the expected value
    • It’ll also implicitly fail on line 20, due to the error div class not matching what dev.to sends to the browser.
  • I’ve disabled the headless feature to show that Puppeteer lets you do that!

Let’s fix the test. Change it to the correct value *and* turn on headless mode:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();
  const loginSelector = 'a[href="/enter"]';
  const submitLoginSelector = '[name="commit"]';
  const errorBoxSelector = '.registration__error-notice';
  try {
    await page.goto('https://dev.to');
    await page.waitForSelector(loginSelector,{ timeout: 3000 });
    await page.click(loginSelector);
    const pageTitle = await page.title();

    if (pageTitle !== 'Welcome! - DEV Community'){
      throw new Error(`Page title ${pageTitle} does not match expected value`);
    }

    await page.waitForSelector(submitLoginSelector,{ timeout: 3000 });
    await page.click(submitLoginSelector);
    await page.waitForSelector(errorBoxSelector,{ timeout: 3000 });
  }catch(e){
    console.error(`Error in test suite: ${e.message}`)
  }finally {
    await browser.close();
  }
})();

Now rerunning the test simply gets you the empty prompt:

tests:DreamMachine % node puppeteer.js
tests:DreamMachine % 

Nice, simple, and clean.

Conclusion

I’ve gone through three different sets of tools for different needs. The best part about these tools is that you can string them all together or pick and choose the ones that are right for you.

I hope the main takeaway is the same: Testing can be painless and even fun!

Selenium can also be used to test email signups with Mailsac.

Questions or comments? Stop by the Mailsac Forums, we’d love to hear from you!

Blog Cover

Guide To Stress Free Email Testing with Next.js

Developing an application that sends emails is straightforward but not without its risks. Ensuring deliverability but not actually having any of those emails land inside real inboxes is a top concern for any developer. Which leads to questions like: “How do you test your application’s outbound email capabilities?” or “How do I manage email testing for free?”

Enter email capture services. While the term “email capture service” tends to focus on the marketing aspects (capturing information from your calls to action, ensuring emails don’t get caught in spam, etc) they also include SMTP deliverability. Mailsac offers an email capture service that addresses the deliverability aspect, specifically not delivering any email to its intended recipient. Effectively a “black hole” where no email should escape to the outside world.

In this post, we’ll walk through a sample application in Next.js that will generate emails and have those emails captured by Mailsac’s email sandboxing service.

Do I Really Need To Do Email Testing?

Some frameworks do come with email previewing capabilities like Rails’ ActionMailer. Said frameworks don’t actually attempt to send anything but instead preview the email on your machine. We recommend real testing during the development and quality assurance phase by using an external SMTP server to mimic the application’s behavior in production.

Testing that capability has to be done safely unless you want to land on Twitter’s trending page for accidentally sending customers an integration test email.

Test Email Sending With A Next.js Application

For the rest of this guide, we’ll focus on wiring up a simple application that will allow users to send an email when a button is pushed from a UI. We’ll then demonstrate the capture of those emails in our development environment.

The components we’ll use are:

Application Creation

While the focus of this guide isn’t a line-by-line walkthrough of the sample code, we’ll focus on the key aspects of the application that mainly involve emailing capabilities.

The application source can be found in our git repository.

1. Application setup

Let’s start by creating a quick next app with tailwind support:

mailsac % npx create-next-app
…
Success! Created nodejs-send-email at /Users/mailsac/code/nodejs-send-email

cd nodejs-send-email
npm install -D tailwindcss postcss autoprefixer @tailwindcss/forms
npm install @headlessui/[email protected] @heroicons/react
npx tailwindcss init -p

The above is the recommended way to install tailwind on Next.js according to their guide.

Configure tailwind.config.js by adding the highlighted lines:

module.exports = {
  content: [
	"./pages/**/*.{js,ts,jsx,tsx}",
	"./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
	extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

and then add tailwind itself to the global CSS file inside styles/global.css and comment out some default CSS created by npx:

/* @media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
  body {
    color: white;
    background: black;
  }
}
*/

@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwind is strictly optional but recommended for easy styling of the frontend.

2. Add the front page UI

Feel free to add your custom frontend code or use the index.js and components/notifications.js react component samples inside our repo.

index.js

import { useEffect, useState } from "react";
import Notification from "../components/notifications";

export default function Index() {
  const [sentEmail, setSentEmail] = useState(false);
  const [emailTo, setEmailTo] = useState("");
  const [emailBody,setEmailBody] = useState("");
  const [resultMessage, setResultMessage] = useState("");

  const sendEmail = () => {
    setSentEmail(true);
  }

  useEffect( () => {
    fetch('/api/send-email',{
      method: 'POST',
      body: JSON.stringify({ to: emailTo, body: emailBody})
    })
    .then( res => res.json())
    .then(response => {
      setResultMessage(response.message)
    })
    .catch(error => console.log(error));    

  },[sentEmail]);

  const SentEmailBanner = sentEmail === true? <Notification message={resultMessage} /> : null;

The second line brings in a component that takes in a message and formats it as a pop-up notification. The useEffect() method sends your email recipient and body input to the backend, which will forward that data to Mailsac’s servers.

3. Sign up for Mailsac’s Email Capture

Mailsac has a free email capture service. All you need is to sign up for an account and generate a key:

Mailsac dashboard
Mailsac Dashboard

4. Add a backend mail handler route

Once you’ve generated and saved your keys, you can place them in a .env file:

.env

MAILSAC_USERNAME=lcanal
MAILSAC_API_KEY=Key generated from above

Following our email capture documentation, we’ll create a backend API route for Next to handle the request:

pages/api/send-email.js

const nodemailer = require("nodemailer");

export default async function handler(req, res) {
  let emailEnvelope = JSON.parse(req.body)
  if (
        req.method === 'POST' 
        && typeof(emailEnvelope.to) !== 'undefined' 
        && emailEnvelope.to !== ''
  ){
    const mailsaUserName = process.env.MAILSAC_USERNAME
    const mailsacAPIKey  = process.env.MAILSAC_API_KEY
  
    const transporter = nodemailer.createTransport({
      host: 'capture.mailsac.com',
      port: 5587,
      // will use TLS by upgrading later in the connection with STARTTLS
      secure: false,
      auth: {
        user: mailsaUserName,
        pass: mailsacAPIKey
      }
    })
  
    try {
      const results = await transporter.sendMail({
        from: '"Sample App" [email protected]',
        to: emailEnvelope.to,
        subject: 'Sample App Send',
        text: emailEnvelope.body
      })
      res.status(200).json(
        { 
          message: "You should now see an email in Mailsac's capture service", 
          response: results.data 
        }
      )
    } catch (error){
      console.log(`ERROR ${error}`)
      res.status(500).json({ message: `${error.response}`, response: error })
    }
  } else {
    return res.status(200).json({message: "No data"});
  }
}

In the highlighted line, we’re ensuring the useEffect() hook gets called with input data before we allow the rest of the function to continue. useEffect() gets called a variety of times in the component lifecycle, and this check is to ensure it was initiated by an end user and not as part of the component mounting.

5. Test driving the app

Fire up the application via

npm run dev

Navigate to http://localhost:3000 and type a text message:

Walking through our sample application form.

Then navigate over to mailsac.com to view the message

Checking the inbox at mailsac.com

6. Capturing other email domains

While that works well as a contrived example, the real value comes when using any arbitrary email in the recipient field:

Full application walkthrough demo, with a free check at mailsac.

Capturing emails outside the mailsac.com domain is extremely valuable when switching between different environments. For example, in the demo application example above, the .env environment file could instead look like

MAILSAC_USERNAME=$MAILSAC_NAME
MAILSAC_API_KEY=$MAILSAC_KEY
MAILSAC_HOST=capture.mailsac.com
MAILSAC_PORT=5587

With the updated send-email.js

const nodemailer = require("nodemailer");

export default async function handler(req, res) {
  let emailEnvelope = JSON.parse(req.body)
  if (
        req.method === 'POST' 
        && typeof(emailEnvelope.to) !== 'undefined' 
        && emailEnvelope.to !== ''
  ){
    const mailsaUserName = process.env.MAILSAC_USERNAME
    const mailsacAPIKey  = process.env.MAILSAC_API_KEY
  
    const transporter = nodemailer.createTransport({
      host: process.env.MAILSAC_HOST,
      port: process.env.MAILSAC_PORT,
      // will use TLS by upgrading later in the connection with STARTTLS
      secure: false,
      auth: {
        user: mailsaUserName,
        pass: mailsacAPIKey
      }
    })
  
    try {
      const results = await transporter.sendMail({
        from: '"Sample App" [email protected]',
        to: emailEnvelope.to,
        subject: 'Sample App Send',
        text: emailEnvelope.body
      })
      res.status(200).json(
        { 
          message: "You should now see an email in Mailsac's capture service", 
          response: results.data 
        }
      )
    } catch (error){
      res.status(500).json({ message: `${error.response}`, response: error })
    }
  } else {
    return res.status(200).json({message: "No data"});
  }
}

The above edits would allow you to deploy to a testing or production environment and the only changes required would be in the .env file. Specifically, the SMTP host and authentication settings.

Conclusion

The above guide just scratches the surface of what you can do with our email services. We provide a unified inbox that allows testers to view their bulk email testing in one unified view and custom domains for those who do not have their own domains with zero setup configurations.

Additionally, we have a guide on using both custom domains and a unified inbox in your system testing. Check it out and tell us what you think on our forums!

Self-Hosted Mailsac UI

This tutorial references code published at https://github.com/mailsac/mailsac-self-hosted-ui

The Mailsac Self-Hosted Temporary Email User Interface is available in a GitHub repository. This project provides a self-hosted user interface for viewing disposable email. It uses mailsac.com as the backend email service.

Mailsac.com Limitations

Mailsac already offers disposable email without a need to sign up for an account. What need does this application meet that Mailsac doesn’t already provide?

Mailsac has limitations on what can be viewed without signing up for an account. Only the latest email in a public mailbox can be viewed without signing in. Mail in a private domain cannot be viewed without signing in with an account that has permissions to the private domain.

Use Cases

There are two use cases that customer’s have brought to our attention that Mailsac’s service doesn’t satisfy. Both stem from a requirement to allow users read-only access to an inbox without the requirement of creating a Mailsac account.

Class Room Use Case

An instructor may want students, who are young in age and don’t have an email address, to sign up for an account with a web service used in the class. The Mailsac Self-Hosted Temporary Email User Interface application provides a simplified interface for students to view email sent to a private mailsac hosted domain without the need to sign up for a mailsac account or email address.

Acceptance Tester Use Case

As part of the sofware development lifecycle there is a need to have software tested by users. Temporary email has long been beneficial to testing. The Mailsac Self-Hosted Temporary Email User Interface makes this easier. Users can test applications using email addresses in a Mailsac hosted private domain without the need to sign up for a Mailsac account. Furthermore, because the application is self-hosted companies can use a reverse proxy to enforce IP allow lists or put the application behind basic authentication.

Running the Mailsac Self-Hosted Email User Interface

Local

With NodeJS installed this application can be run with the following commands.

npm install && npm run build
MAILSAC_KEY=YOUR_MAILSAC_API_KEY npm run start

You will need to generate a Mailsac API key. To generate or manage API Keys use the API Keys page.

The application is now running and can be accessed via a web browser at http://localhost:3000 .

Any public or private Mailsac hosted address the API key has access to can be viewed by entering the email address in the text box and selecting “view mail”.

Screenshot of Application with no domain defined

Domain Option

You can prepopulate the domain by using the NEXT_PUBLIC_MAILSAC_CUSTOM_DOMAIN environment variable.

NEXT_PUBLIC_MAILSAC_CUSTOM_DOMAIN=example.mailsac.com npm run build
MAILSAC_KEY=YOUR_MAILSAC_API_KEY npm run start
Screenshot of pre-populated domain

Vercel Hosted

Vercel is a platform as a service provider. Their service makes running your own Next.js application easy.

The Vercel Getting Started guide is easy to follow.

  1. Fork this repo.
  2. Sign up for a Vercel account
  3. Grant Vercel permissions to read all your repos or choose to grant permission on the forked repo
  4. Import forked repository into Vercel
Screenshot showing import of forked repo
  1. Configure MAILSAC_KEY environment variable
Screenshot of environment variables

  1. Deploy application
Screenshot showing deployment success

After a successful deployment you can click on the image of the application to be taken to the live application.

NOTE There is currently no authentication on this application. Anyone with the URL will be able to view emails and domains associated with the Mailsac API key that was used. Operations will be tracked in the Mailsac account in which the API key is associated with.

You are free to deploy this app however you like. Please keep the attribution to Mailsac.

Email Integration Tests Using Java

Mailsac provides a REST API to fetch and read email. The REST API also allows you to reserve an email address that can forward messages to another mailsac email address, Slack, WebSocket, or webhook

This article describes how to integrate with Mailsac using Java and the JUnit testing framework. The JavaMail API will be used to send email via SMTP.

What is JUnit?

JUnit is a unit testing framework for the Java programming language. The latest version of the framework, JUnit 5, requires Java 8 or above. It supports testing using a command-line interface, build automation tools, and IDEs.

JUnit can be used to test individual components of code to ensure that each unit is performing as intended.

Setting Up the Environment

Depending on the environment, there are multiple ways to run tests. Testing using the command and JUnit are included in this example.

Testing Using Command-Line

Running tests from the command-line requires the ConsoleLauncher application(junit-platform-console-standalone-1.7.2.jar). JUnit ConsoleLauncher is published in the Maven Central repository under the junit-platform-console-standalone directory.

  1. Navigate to the Maven Central directory
  2. Download junit-platform-console-standalone-1.7.2.jar.
  3. Create a directory for the project: mkdir mailsac-tests.
  4. Move the jar file you downloaded into the directory mailsac-tests.
  5. Create a directory inside mailsac-testsmkdir test.

    Note: mailsac-tests/test will contain your source code.

JUnit Testing Introduction

This code example shows basic usage of the JUnit testing framework.

Inside the directory mailsac-tests/test, create a java file: touch TestClass.java.

Add the following code snippet to ./mailsac-tests/test/TestClass.java

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("tests truth")
class TestClass {

    @Test
    @DisplayName("true equals true")
    void trueEqualsTrue() {
        // The assertTrue method asserts that the supplied condition is true.
        // static void assertTrue(condition)
        assertTrue(true);
    }

    @Test
    @DisplayName("false equals false")
    void falseEqualsFalse() {
        // The assertEquals method asserts that expected and actual are equal.
        // static void assertEquals(expected, actual)
        assertEquals(false, false);
    }

}

@Test Denotes that a method is a test.

@DisplayName Declares a custom display name for the test class or test method.

Refer to JUnit annotations and JUnit Assertions for further reading.

Running JUnit Tests From The Command-Line

  1. Inside the directory mailsac-tests, compile the test:javac -verbose -cp junit-platform-console-standalone-1.7.2.jar -d test test/TestClass.java.
  2. Then run:java -jar junit-platform-console-standalone-1.7.2.jar --class-path test --scan-class-path.

The output should appear similar to this:

╷
├─ JUnit Jupiter ✔
│  └─ tests truth ✔
│     ├─ false equals false ✔
│     └─ true equals true ✔
└─ JUnit Vintage ✔

Test run finished after 92 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         2 tests found           ]
[         0 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

The first section of output shows the name of the unit test (tests truth) and the test names (true equals true and false equals false). The checkmark next to the test name indicates it was successful.

The second section of output shows a summary of the test results.

Testing Using Build Tools

Testing from build automation tools, like Maven, is another option. In many ways, using build tools is the best option. For instance, they provide a standard directory layout that encourages industry standard naming conventions.

Maven abstracts many underlying mechanisms allowing developers to run a single command for validating, compiling, testing, packaging, verifying, installing, and deploying code.

This section will describe how to set up Maven for building, managing, and testing a project.

  1. Navigate to the Apache Maven download page and follow the installation instructions. If you have Homebrew you can install Maven using the command: brew install maven.
  2. After installing Maven, run on the command-line to initialize the directory mailsac-integration-test-java as a maven managed project:
mvn archetype:generate \
    -DgroupId=com.mailsac.api \
    -DartifactId=mailsac-integration-test-java \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 \
    -DinteractiveMode=false
  1. Navigate into the directory: cd mailsac-integration-test-java
  2. Update the <dependencies> and <build> sections of pom.xml with the following xml.
<!-- ... -->
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>
<!-- ... -->
<build>
  <pluginManagement>
    <!-- ... -->
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
      <plugin>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
    </plugins>
    <!-- ... -->
  </pluginManagement>
</build>
<!-- ... -->
  1. Edit the AppTest.java file: $EDITOR src/test/java/com/mailsac/api/AppTest.java
package com.mailsac.api;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class TestClass {

    @Test
    void trueEqualsTrue() {
        // The assertTrue method asserts that the supplied condition is true.
        // static void assertTrue(condition)
        assertTrue(true);
    }

    @Test
    void falseEqualsFalse() {
        // The assertEquals method asserts that expected and actual are equal.
        // static void assertEquals(expected, actual)
        assertEquals(false, false);
    }

}
  1. In the directory mailsac-integration-test-java, run mvn clean package. This command deletes the folder target , packages the project into a new target folder, and runs a unit test.
  2. Tests can be manually run using the command mvn test in the mailsac-integration-test-java directory.The output should appear similar to:
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mailsac.api.TestClass
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.029 s - in com.mailsac.api.TestClass
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Mailsac Java Integration Test

This section describes how to leverage Mailsac and JUnit to test mail delivery. Emails will be sent to Mailsac using SMTP and email delivery will be validated with JUnit.

There are 3 additional libraries that will be used:

Integration Test Example

  1. With Maven, add the following dependencies to pom.xml
    If you are not using Maven include the JAR files in the classpath:

https://mvnrepository.com/artifact/com.mashape.unirest/unirest-java/1.4.9

https://mvnrepository.com/artifact/com.sun.mail/javax.mail/1.6.2

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.12.5

<!-- ... -->
<dependencies>
  <!-- ... -->
  <dependency>
    <groupId>com.mashape.unirest</groupId>
    <artifactId>unirest-java</artifactId>
    <version>1.4.9</version>
  </dependency>
  <dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.5</version>
  </dependency>
</dependencies>
<!-- ... -->
  1. Edit the AppTest.java file: $EDITOR src/test/java/com/mailsac/api/AppTest.java

    Import the required modules
package com.mailsac.api;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
  1. Acquire a Mailsac API key and configure SMTP sending. Export these parameters as environment variables:
export MAILSAC_API_KEY=your_mailsac_key;
export [email protected]
export [email protected]
export SMTP_USERNAME=your_smtp_username
export SMTP_PASSWORD=your_smtp_password
export SMTP_HOST=smtp.example.com
public class AppTest {
    // MAILSAC_API_KEY environment variable. Generated by mailsac. See
    // https://mailsac.com/api-keys
    static String mailsacAPIKey = "";
    // MAILSAC_TO_ADDRESS environment variable. Who you're sending an email to.
    static String mailsacToAddress = "";
    // SMTP_FROM_ADDRESS environment variable. Necessary if you are sending
    // through out.mailsac.com (unlikely - you most likely will replace
    // sendMail() below.
    static String fromAddress = "";
    // SMTP_USERNAME environment variable. Required for authenticated SMTP sending
    static String smtpUserName = "";
    // SMTP_PASSWORD environment variable. Required for authenticated SMTP sending
    static String smtpPassword = "";
    // SMTP_HOST environment variable. Hostname of your SMTP server
    static String smtpHost = "";
    // SMTP_PORT environment variable. Port used for SMTP sending
    static int smtpPort = 587;

    @BeforeAll
    static void setup() throws Exception {
        mailsacAPIKey = System.getenv().get("MAILSAC_API_KEY");
        mailsacToAddress = System.getenv().get("MAILSAC_TO_ADDRESS");
        fromAddress = System.getenv().get("SMTP_FROM_ADDRESS");
        smtpUserName = System.getenv().get("SMTP_USERNAME");
        smtpPassword = System.getenv().get("SMTP_PASSWORD");
        smtpHost = System.getenv().get("SMTP_HOST");
        if (System.getenv().get("SMTP_PORT") != null) {
            Integer.parseInt(System.getenv().get("SMTP_PORT"));
        }
        if (mailsacAPIKey == null || mailsacToAddress == null || fromAddress == null) {
            throw new Exception("Missing environment variable setup!");
        }
        if (smtpUserName == null || smtpPassword == null || smtpHost == null) {
            throw new Exception("Missing SMTP environment variables");
        }
        System.out.println(mailsacAPIKey);
        System.out.println(mailsacToAddress);
        System.out.println(fromAddress);
    }
}
  1. Add a purgeInbox() method which makes a DELETE request to api/addresses/{email}/messages/(messageId}.

    This section of code should be added to the existing AppTest class.
public class AppTest {
  //...
 @BeforeEach
 @AfterEach
 // purgeInbox cleans up all messages in the inbox before and after running each
 // test,
 // so there is a clean state.
 void purgeInbox() throws UnirestException, JsonProcessingException {
     HttpResponse<String> response = Unirest
             .get(String.format("https://mailsac.com/api/addresses/%s/messages", mailsacToAddress))
             .header("Mailsac-Key", mailsacAPIKey)
             .asString();

     // Parse JSON
     ObjectMapper objectMapper = new ObjectMapper();
     Object[] messagesArray = objectMapper.readValue(response.getBody(), Object[].class);

     for (int i = 0; i < messagesArray.length; i++) {
         JsonNode m = objectMapper.convertValue(messagesArray[i], JsonNode.class);
         String id = m.get("_id").asText();
         System.out.printf("Purging inbox message %s\n", id);
         Unirest.delete(String.format("https://mailsac.com/api/addresses/%s/messages/%s", mailsacToAddress, id))
                 .header("Mailsac-Key", mailsacAPIKey)
                 .asString();
     }
 }
  //...
}
  1. Implement a sendMail() method which sends an email. This section will likely likely be different depending on your use case. For example, you may be sending emails via your web application or via an email campaign.
public class AppTest {
    //...
    static void sendMail(String subject, String textMessage, String htmlMessage)
            throws UnsupportedEncodingException, MessagingException {
        Session session = Session.getDefaultInstance(new Properties());
        javax.mail.Transport transport = session.getTransport("smtp");
        MimeMessage msg = new MimeMessage(session);

        // set message headers
        msg.addHeader("Content-type", "text/HTML; charset=UTF-8");
        msg.addHeader("format", "flowed");
        msg.addHeader("Content-Transfer-Encoding", "8bit");

        msg.setFrom(fromAddress);
        msg.setReplyTo(InternetAddress.parse(fromAddress));
        msg.setSubject(subject, "UTF-8");
        msg.setText(textMessage, "UTF-8");
        msg.setContent(htmlMessage, "text/html");

        msg.setSentDate(new Date());

        msg.setRecipients(Message.RecipientType.TO, mailsacToAddress);
        msg.saveChanges();
        System.out.println("Email message is ready to send");
        transport.connect(smtpHost, smtpPort, smtpUserName, smtpPassword);
        transport.sendMessage(msg, msg.getAllRecipients());

        System.out.println("Email sent successfully");
    }
    // ...
}
  1. Add test. Use a for loop to check if the message was received by scanning the recipient inbox periodically. If the recipient inbox is not empty, and a message was found, the test verifies the message content:

    This test uses the Mailsac API endpoint /api/addresses/{email}/messages which lists all messages in an inbox.
public class AppTest {
    //...
    @Test
    void checkEmailWithLink() throws MessagingException, UnirestException, IOException, InterruptedException {
        sendMail("Hello!", "Check out https://example.com", "Check out <a href='https://example.com'>My website</a>");
        // Check inbox for the message up to 10x, waiting 5 seconds between checks.
        found: {
            for (int i = 0; i < 10; i++) {
                // Send request to fetch a JSON array of email message objects from mailsac
                HttpResponse<String> response = Unirest
                        .get(String.format("https://mailsac.com/api/addresses/%s/messages", mailsacToAddress))
                        .header("Mailsac-Key", mailsacAPIKey)
                        .asString();

                // Parse JSON
                ObjectMapper objectMapper = new ObjectMapper();
                Object[] messagesArray = objectMapper.readValue(response.getBody(), Object[].class);

                System.out.printf("Fetched %d messages from Mailsac for address %s\n", messagesArray.length,
                        mailsacToAddress);
                eachMessage: {
                    for (int m = 0; m < messagesArray.length; m++) {
                        // Convert object into JSON to fetch a field
                        JsonNode thisMessage = objectMapper.convertValue(messagesArray[m], JsonNode.class);

                        // After a message is found, the JSON object is checked to see if the link was
                        // sent correctly
                        assertTrue(thisMessage.get("links").toString().contains("https://example.com"),
                                "Missing / Incorrect link in email");

                        System.out.printf("Message id %s contained the correct link\n",
                                thisMessage.get("_id").asText());

                        return; // end the tests
                    }
                }

                System.out.println("Message not found yet, waiting 5 secs");
                Thread.sleep(5000);
            }

            // Fail the test if we haven't reached assertTrue above
            fail("Never received expected message!");
        }
    }
    // ..
}
  1. At this point, the code is complete. Package the project: mvn clean package. This will also run a test.

    Subsequent changes to the source file do not require you to run mvn clean package again. Instead, run mvn test.

    The output should appear similar to this:
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mailsac.api.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.148 s s - in com.mailsac.api.AppTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

GitHub Repository

If you encounter any difficulties, git clone https://github.com/mailsac/mailsac-integration-test-java. Make edits as necessary, and run mvn package.

Alternatively, if your tests fail because of error codes when making requests to the Mailsac API, please refer to the API Specification for further reading.

Next Steps

The Mailsac API Specification has generated code examples in Java + Unirest for making requests. It also has code examples in other languages.

This example can be adjusted to get all private email addresses for an account and purge their inboxes if necessary.

Please visit our forums if you have any questions!

Plus-addressing is supported by all Mailsac inboxes

When you send to any inbox @mailsac.com, if a + plus symbol is included, we remove that symbol and everything after it.

jeff+12345asdf@mailsac.com

will be delivered to

[email protected]

Many email services including Gmail, iCloud and Fastmail support stripping the + plus symbol and everything after it in the local-part of the address (everything before the @ symbol).

Plus-addressing has long been a useful feature to segment user accounts across services. At Mailsac we offer a variety of disposable email and forwarding utilities that are designed for software QA engineers and developers. Things like forwarding all messages in a domain to a single address, or automatically routing email to webhooks or slack, are really easy – may not even require DNS setup.

Speed Up Integration Tests Using WebSockets

All the code shown in this article is published at https://github.com/mailsac/mailsac-integration-test-examples

Integration tests identify errors between systems. These tests can be slow to run because of the interactions between multiple systems.

Mailsac can facilitate integration testing between web apps and transactional email services.

This article explains how to use WebSockets to make your email integration tests faster and simpler than with REST API polling.

Explaining the differences: REST APIs vs WebSockets

A REST API call uses a HTTP request for creating, reading, updating, and deleting objects. The HTTP connection between the client and server is short lived. An example of this is the List Messages In Inbox endpoint. The endpoint will return JSON formatted information about email messages in a Mailsac Inbox. Each time a client checks for new messages a new HTTP connection will be used.

A WebSocket is a persistent connection between a client and server providing full-duplex communication. By reusing an established connection there is no need to poll the REST API for changes, instead data can be pushed to the client in real time. WebSockets often listen on port 80/443 but does not use HTTP, except for the initial connection handshake.

REST API Polling Examples

The examples below use the “old way” – hit the Mailsac REST API and “poll” the inbox for new messages.

Polling is reliable and familiar for API programmers.

However, this approach can result in significant delays between when the email was received by Mailsac and when the test checks for a new message. Every few seconds, you ask the server if there are new messages.

Using a Web Socket Instead of Polling

What if the server could notify you that there are new messages? That’s where WebSockets come in.

This code example uses Node.js and Mocha test framework. Portions of the code example are presented below.

Steps to Validate Email Contents Using a WebSocket

Here’s how you can get notified of a new email message:

  1. Establish a WebSocket connection with Mailsac
  2. Send an email using SMTP to a private Mailsac address
  3. Receive content of the email over the established WebSocket
  4. Validate the content of the email
  5. Send a second email using SMTP to a private Mailsac addrress
  6. Receive content of second email over the established WebSocket
  7. Validate the content of the second email
  8. Delete both email messages
  9. Close WebSocket connection

Test Configuration

This test requires several variables be defined. They can be set by editing the script or by setting environment variables.

const mailsacAPIKey = process.env.MAILSAC_API_KEY || ''; // Generated by mailsac. See https://mailsac.com/api-keys
const mailsacToAddress = process.env.MAILSAC_TO_ADDRESS || ''; // Mailsac email address where the email will be sent
const smtpUserName = process.env.SMTP_USER || ''; // Username for smtp server authentication
const smtpPassword = process.env.SMTP_PASSWORD || ''; // Password for smtp server authentication
const smtpHost = process.env.SMTP_HOST || ''; // hostname of the smtp server
const smtpPort = process.env.SMTP_PORT || 587; // port the smtp is listening on

Setup: Configure Mailsac Address for WebSocket Forwarding

The Mailsac address used in this example needs to have WebSocket forwarding enabled on it. Any messages sent to the email address will be forwarded by the Mailsac WebSocket server.

To enable WebSocket forwarding the Mailsac address must be private. Private addresses have additional features such as forwarding to Slack, forwarding to a Webhook, and forwarding to a WebSocket. Select the “Settings” button next to the email address you want to configure from Manage Owned Email Addresses. Select the check box to “Enabled forwarding all incoming email via web socket” and select “Save Settings”.

1. Establish WebSocket Connection

The NPM package ws is a WebSocket client used to connect to the Mailsac WebSocket service.

The Websocket connection to Mailsac is established on lines 12-14. The ws package will only reject a Promise if it fails to connect to the WebSocket server due to a network error. Wrapping the connection in a Promise allows for additional validations.

The Mailsac WebSocket server will send the message ({"status":200,"msg":"Listening","addresses":["[email protected]"]}) after the initial connection. In lines 16-26 the initial message is parsed and checked for value of the property status. The Promise is rejected if the initial status message is not received or does not have a status code of 200.

const mailsacAPIKey = process.env.MAILSAC_API_KEY || ''; // Generated by mailsac. See https://mailsac.com/api-keys
const mailsacToAddress = process.env.MAILSAC_TO_ADDRESS || ''; // Mailsac email address where the email will be sent

describe("send email to mailsac", function () {

  // Open websocket waiting for email. This websocket will be reused for tests in this file.
  before(() => {
    return new Promise((resolve, reject) => {
      ws = new WebSocket(
        `wss://sock.mailsac.com/incoming-messages?key=${mailsacAPIKey}&addresses=${mailsacToAddress}`
      );
      let wsMessage; // message response object
      ws.on("message", (msg) => {
        try {
          wsMessage = JSON.parse(msg);
        } catch {
          assert(wsMessage, "Failed to parse JSON from websocket message");
        }
        if (wsMessage.status != 200) {
          reject(new Error("connection error: " + wsMessage.error));
          return;
        }
        resolve(wsMessage);
      });
      ws.on("error", (err) => {
        reject(err);
      });
    });
  });
});

2. Send email using SMTP

The NPM package nodemailer is used to send a test email via SMTP.

The connection to the STMP server is configured in lines 4-10. Most SMTP servers will require authentication.

The email’s to, from, subject, and content are set in lines 14-18. The email will be sent to the address defined in the configuration at the beginning of the script or the environment variable MAILSAC_TO_ADDRESS. The email will include a link to the website https://example.com.

  it("sends email with link to example.com website", async () => {
    // create a transporter object using the default SMTP transport
    const transport = nodemailer.createTransport({
      host: smtpHost,
      port: smtpPort,
      auth: {
        user: smtpUserName,
        pass: smtpPassword,
      },
    });
    // send mail using the defined transport object
    const result = await transport.sendMail({
      from: smtpUserName, // sender address
      to: mailsacToAddress, // recipient address
      subject: "Hello!",
      text: "Check out https://example.com",
      html: "Check out <a href https://example.com>My website</a>",
    });
  });

3. Receive Message via WebSocket

Once the email arrives, Mailsac will send a JSON formatted version of the email on the WebSocket established earlier in this example. ws.on("message", (msg) => { ... } is a function that will run when new message is sent by the WebSocket server. The msg is parsed as JSON. Then the Promise will resolve if the message has a to property. The existence of the to property is checked to make sure the message sent by the WebSocket server was an email and not a status message. The await keyword will cause the test to wait until a message is sent over the WebSocket or the test times out.

const wsMessage = await new Promise((resolve) => {
  ws.on("message", (msg) => {
    const wsResponse = JSON.parse(msg);
    if (wsResponse.to) {
      resolve(wsResponse);
    }
  });
});

Example JSON formatted message

{

    "status": 200,
    "email": 

{

    "text": "string",
    "body": "string",
    "raw": "string",
    "headers": { },
    "_id": "m3phnJ2ag3example-0",
    "from": 

[

    {
        "name": "string",
        "address": "string"
    }

],
"to": 
[

    {
        "name": "string",
        "address": "string"
    }

],
"cc": 
[

    {
        "name": "string",
        "address": "string"
    }

],
"bcc": 
[

    {
        "name": "string",
        "address": "string"
    }

],
"subject": "string",
"savedBy": "string",
"originalInbox": "string",
"inbox": "[email protected]",
"domain": "example.com",
"received": "1985-04-12T23:20:50.52Z",
"size": 0,
"attachments": 
[

    "5ea9c924627e68f988c9c7ca44340892"

],
"ip": "string",
"via": "string",
"folder": "inbox",
"labels": 
[

    "string"

],
"read": true,
"rtls": true,
"links": 

        [
            "string"
        ],
        "spam": 0.345
    }

}

4. Validate Content of the Email

The assert package is used to validate the contents of the email. The subject and text properties are assigned to new variables. assert.equal(subject, "Hello!"); will cause an exception if subject is not equal to Hello!. The test framework Mocha will interpret this as a failure and the test will fail. Likewise, if the variable email_text is not Check out https://example.com the test will fail.

const subject = wsMessage.subject;
const email_text = wsMessage.text;
assert.equal(subject, "Hello!");
assert.equal(email_text, "Check out https://example.com");

5-7. Send Another Email and Validate Its Content

The reason to send a second email is to demonstrate that the Mailsac WebSocket connection can be reused. This second test will reuse the WebSocket connection established (variable name ws) in the before() block in step 1. The only difference between the first and second test is the content of the email.

// Sends a second email reusing the websocket.
it("sends email with link to unsubscribe.example.com website", async () => {
  const transport = nodemailer.createTransport({
    host: smtpHost,
    port: smtpPort,
    auth: {
      user: smtpUserName,
      pass: smtpPassword,
    },
  });
  const result = await transport.sendMail({
    from: smtpUserName, // sender address
    to: mailsacToAddress, // recipient address
    subject: "Unsubscribe",
    text: "Click the link to unsubscribe https://unsubscribe.example.com",
    html: "Check out <a href https://example.com>My website</a>",
  });

  console.log("Sent email with messageId: ", result.messageId);

  const wsMessage = await new Promise((resolve) => {
    ws.on("message", (msg) => {
      const wsResponse = JSON.parse(msg);
      if (wsResponse.to) {
        resolve(wsResponse);
      }
    });
  });

  assert(wsMessage, "Never received messages!");

  const subject = wsMessage.subject;
  const email_text = wsMessage.text;
  assert.equal(subject, "Unsubscribe");
  assert.equal(
    email_text,
    "Click the link to unsubscribe https://unsubscribe.example.com"
  );
});

8. Delete Emails to Prevent Leaky Tests

An after() block will run after the tests have completed. The REST API Endpoint – Delete All Messages In An In Inbox is called to delete the test emails. By deleting all the test emails, it prevents these emails from being fetched in another test, which could impact the results of another test. It is best practice to clean up tests after they ran.

The NPM package supertest is used to make the REST call to delete the messages. Virtually any HTTP client library could be used to do this. Feel free to use the HTTP client library you feel most comfortable with.

afte(() =>
  request("https://mailsac.com")
    .delete(`/api/addresses/${mailsacToAddress}/messages`)
    .set("Mailsac-Key", mailsacAPIKey)
    .expect(204)
);

9. Close WebSocket Connection

An after() block is used to close the WebSocket connection after all the tests have completed. It is best practice to close all connections on the termination of the test.

// close websocket after all tests finish
after(() => ws.close());

Next Steps

The use of WebSockets helps speed up tests and uses API calls more efficiently.

See the WebSocket Test Page to see a WebSocket in action in your browser. This page includes a basic code example usage ofoffor a WebSocket client and is a great starting point before divining into integration testing using WebSockets.

If you have questions about this example or the Mailsac WebSocket service please post on https://forum.mailsac.com