Test your 2FA with Selenium and Mailsac

Verifying 2FA Code Delivery via Email with Selenium

Related: how to read OTP codes and magic links from email in Selenium, Playwright, Cypress and Python tests.

Login failures lock users out of your app. From forgotten passwords to broken authentication flows, a bad login experience is a nightmare for both users and support teams.

While a simple login screen seems easy to test, complexity grows fast when you introduce two-factor authentication (2FA), magic links, SSO, or team-based accounts. Can your tests keep up?

In this guide, we’ll show you how to automate 2FA email verification using Selenium and Mailsac. You’ll send a real email, extract the one-time password (OTP) programmatically, and validate authentication—ensuring your app’s login works flawlessly.

New to Selenium email testing? Start with our Selenium email testing guide. It covers headless Chrome setup, a unique test address for every run, waiting only for mail that arrives after the test acts, and running the suite in CI. This post goes deeper on the 2FA login flow.

Selenium & Mailsac Prep

You’re already familiar with Selenium—after all, it’s a core tool for browser automation. (Fun fact: we’re a proud Selenium sponsor!)

Have you worked directly with WebDriver? WebDriver is what allows Selenium to control a browser just like a real user—clicking buttons, filling out forms, and navigating pages. For our automated 2FA test, we’ll use WebDriver to interact with the login flow, retrieve the OTP from an email, and complete authentication.

Step 1: Install Selenium for Javascript

Let’s start by installing Selenium via npm:

npm init
npm install selenium-webdriver chromedriver dotenv

You’ll also need to install chromedriver by manually downloading it from Google or from your package manager. For example for macOS:

brew install chromedriver

Step 2: Sign Up At Mailsac for Email Testing

Mailsac provides disposable email addresses and an API to fetch incoming emails. We’ll use it to verify that a 2FA email is received and contains the correct code.

Why Use Mailsac?

  • Eliminate reliance on real email accounts → No need to clutter your inbox with test emails or use shared email accounts.
  • Prevents rate-limiting & spam issues → Disposable email addresses is the core feature and is extremely quick.
  • Automates email verification → You can fetch emails instantly via API or receive them over web sockets.

Setting Up Mailsac API Access

1. Create a Free Mailsac Account:

  • Go to mailsac.com and sign up.
  • Navigate to your Dashboard then Credentials and API Keys to generate an API key.

2. Install Mailsac’s API Client

Install the official mailsac API client:

npm install @mailsac/api dotenv

3. Store Your API Key Securely

Never hardcode your API key. Use environment variables by creating a .env file:

MAILSAC_API_KEY=your-api-key-here

Now that we’ve installed Selenium and set up our Mailsac account, we can start writing our test script to automatically log in, retrieve the 2FA code, and verify it in our application.

Our Sample App

Let’s introduce our sample application that we’re going to test, called “Active Forums”.
It’s an early version of a forum application. It’s got the basic logic of Sign In, the landing page that shows you all your posts, and the Admin Panel

Pages We’re Interested In Testing

Sign In

Our sample app login screen.

One Time Password

Our sample app otp screen.

Landing Page

Our sample app dashboard screen.

Automating The Entire Login Process

Let’s automate the process of logging in, retrieving the 2FA code, and verifying it.

Step 1: Automate the Login Process

Our first task is to navigate to the login page, enter the username and password, and submit the form.

1.1 Initialize Selenium WebDriver

const { Builder, By, until } = require('selenium-webdriver');
require('dotenv').config();
const { Mailsac } = require('@mailsac/api');

async function login(emailAddress,accountPassword) {
    let driver = await new Builder().forBrowser('chrome').build();
    try {
        await driver.get("http://localhost:3000/users/sign_in");
        await driver.findElement(By.id("email")).sendKeys(emailAddress);
        await driver.findElement(By.id("password")).sendKeys(accountPassword);
        await driver.findElement(By.id("sign-in")).click();

    } catch (error) {
        console.error("Login test failed:", error);
    }
    return driver;
}

Here we:

  • Open the login page.
  • Enter the incorrect username and password into the form fields.
  • Click the login button.
  • Enter the correct login details
  • Return the driver back for the 2FA check.

Step 2: Retrieve the 2FA Code via Mailsac API

Once the user is on the 2FA screen, we need to fetch the one-time code from the email inbox.

Load the API in the script:

require('dotenv').config();
const { Mailsac } = require('@mailsac/api');

Now, we write a function to check the inbox and extract the 6-digit code from the email.

async function get2FACode(emailAddress) {
    try {
        console.log(`Checking inbox for: ${emailAddress}`);

        // Fetch the latest messages from the inbox
        const mailsac = new Mailsac({ headers: { "Mailsac-Key": process.env.MAILSAC_API_KEY } });
        const results = await mailsac.messages.listMessages(emailAddress);
        const messages = results.data;

        if (!messages.length) {
            console.log("No 2FA email received yet.");
            return null;
        }

        // Get the latest message ID
        const latestMessageId = messages[0]._id;

        // Fetch email body
        // const emailBody = await mailsac.messages.getMessage(latestMessageId);
        const emailBody = await mailsac.messages.getBodyPlainText(emailAddress,latestMessageId,{ download : true });
        // Extract 6-digit 2FA code using regex
        const match = emailBody.data.match(/\b\d{6}\b/);
        if (match) {
            console.log(`2FA Code found: ${match[0]}`);
            return match[0];
        } else {
            console.log("No 2FA code found in email.");
            return null;
        }
    } catch (error) {
        console.error("Error retrieving 2FA email:", error);
    }
}

What This Does:

  • Checks the Mailsac inbox for a new email.
  • Extracts the latest email’s content.
  • Uses a regex pattern to find the 6-digit 2FA code inside the email body.

Step 3: Input the 2FA Code in the Login Form

Now that we have the 2FA code, we return to Selenium to enter it into the form.

async function enter2FACode(driver, emailAddress) {
    let attempts = 0;
    let otpCode = null;

    // Polling for 2FA code with a maximum of 5 attempts
    while (!otpCode && attempts < 5) {
        otpCode = await get2FACode(emailAddress);
        if (!otpCode) {
            console.log("Waiting for 2FA code...");
            await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds before retrying
            attempts++;
        }
    }

    if (!otpCode) {
        console.error("Failed to retrieve 2FA code after multiple attempts.");
        return;
    }

    try {
        // Enter the 2FA code into the form
        await driver.findElement(By.id("otp_code")).sendKeys(otpCode);

        // Submit the form
        await driver.findElement(By.name("commit")).click();
        console.log("2FA code submitted, verifying login...");

        // Wait for successful login redirect
        await driver.wait(until.urlContains("/"), 10000);
        console.log("Login successful!");
    } catch (error) {
        console.error("2FA verification failed:", error);
    }
}

What This Does:

  • Retries up to 5 times to fetch the 2FA code from the email inbox.
  • If no code is found, waits 5 seconds before trying again.
  • Once the code is found, inputs it into the form and submits it.
  • Waits for the dashboard page to confirm successful login.

Step 4: Run the Full Automated Test

Now, we combine everything into a single test flow.

(async function testLogin2FA() {
    const testEmail = "[email protected]";
    const testCreds = "password123";

    // Step 1: Log in
    const driver = await login(testEmail,testCreds);

    // Step 2: Retrieve and enter 2FA code
    if (driver) {
        await enter2FACode(driver, testEmail);
        await driver.quit();
    }
})();

Full main.js File

Here’s what our full file now looks like:

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

require('dotenv').config();
const { Mailsac } = require('@mailsac/api');

async function login(emailAddress,accountPassword) {
    let driver = await new Builder().forBrowser('chrome').build();

    try {
        await driver.get("http://localhost:3000/users/sign_in");
        await driver.findElement(By.id("email")).sendKeys(emailAddress);
        await driver.findElement(By.id("password")).sendKeys(accountPassword);
        await driver.findElement(By.id("sign-in")).click();

    } catch (error) {
        console.error("Login test failed:", error);
    }
    return driver;
}

async function get2FACode(emailAddress) {
    try {
        console.log(`Checking inbox for: ${emailAddress}`);

        // Fetch the latest messages from the inbox
        const mailsac = new Mailsac({ headers: { "Mailsac-Key": process.env.MAILSAC_API_KEY } });
        const results = await mailsac.messages.listMessages(emailAddress);
        const messages = results.data;

        if (!messages.length) {
            console.log("No 2FA email received yet.");
            return null;
        }

        // Get the latest message ID
        const latestMessageId = messages[0]._id;

        // Fetch email body
        // const emailBody = await mailsac.messages.getMessage(latestMessageId);
        const emailBody = await mailsac.messages.getBodyPlainText(emailAddress,latestMessageId,{ download : true });
        // Extract 6-digit 2FA code using regex
        const match = emailBody.data.match(/\b\d{6}\b/);
        if (match) {
            console.log(`2FA Code found: ${match[0]}`);
            return match[0];
        } else {
            console.log("No 2FA code found in email.");
            return null;
        }
    } catch (error) {
        console.error("Error retrieving 2FA email:", error);
    }
}

async function enter2FACode(driver, emailAddress) {
    let attempts = 0;
    let otpCode = null;

    // Polling for 2FA code with a maximum of 5 attempts
    while (!otpCode && attempts < 5) {
        otpCode = await get2FACode(emailAddress);
        if (!otpCode) {
            console.log("Waiting for 2FA code...");
            await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds before retrying
            attempts++;
        }
    }

    if (!otpCode) {
        console.error("Failed to retrieve 2FA code after multiple attempts.");
        return;
    }

    try {
        // Enter the 2FA code into the form
        await driver.findElement(By.id("otp_code")).sendKeys(otpCode);

        // Submit the form
        await driver.findElement(By.name("commit")).click();
        console.log("2FA code submitted, verifying login...");

        // Wait for successful login redirect
        await driver.wait(until.urlContains("/"), 10000);
        console.log("Login successful!");
    } catch (error) {
        console.error("2FA verification failed:", error);
    }
}

// Main Test Logic
(async function testLogin2FA() {
    const testEmail = "[email protected]";
    const testCreds = "password123";

    // Step 1: Log in
    const driver = await login(testEmail,testCreds);

    // Step 2: Retrieve and enter 2FA code
    if (driver) {
        await enter2FACode(driver, testEmail);
        await driver.quit();
    }
})();

Now try running the whole login test:

node main.js

And just like that we now have an automated way to test outbound emails, logins, one time passwords and correct landing page location!

Automate Your 2FA Testing with Confidence

Login authentication is a critical part of any web application, and failing to test it properly can lead to frustrated users and costly support issues. By combining Selenium for browser automation and Mailsac for email-based 2FA verification, we’ve built a fully automated test that ensures your app’s login process works smoothly.

With this setup, your testing team can:

  • Programmatically retrieve real one-time passwords (OTPs) from email
  • Automate the entire login flow, including 2FA verification
  • Reduce reliance on manual testing and catch authentication issues early

This approach not only saves time but also improves the reliability of your authentication system. You can now integrate this script into your CI/CD pipeline to ensure every update maintains seamless login functionality.

Visit our forums if you have any questions. Now it’s your turn—try running the script, tweak it for your specific use case, and start automating your login tests today!

Test Smarter : Load Testing Your Email Systems with Mailsac

Need to read every email in a high-volume run, such as thousands of signup confirmations in a few minutes? See email load testing with Mailsac: what a standard plan handles, how to size a run, and when to request a high-throughput evaluation. The count-only feature below is for measuring how fast your own system sends.

Email performance can make or break your app—especially during high-demand events like Black Friday sales, onboarding campaigns, or critical notifications. That’s why we’re excited to introduce our new Email Load Testing feature: a stress-testing tool designed to help you simulate real-world email loads quickly, efficiently, and affordably.

Now you can confidently validate your email systems without impacting production environments or breaking the bank. We’ll walk you through the essentials of email load testing and show you how to use a simple script to make the most of our new feature.


Why Use Mailsac’s Email Load Testing?

Traditional load testing often comes with challenges:

  • Expensive email credits for large-scale tests.
  • Limited metrics that fail to offer actionable insights.
  • Risk of production impact during testing.

We specifically focused on solving these problems. By isolating your tests with unique, disposable subdomains, it makes sure that you have:

  • Accurate, real-time metrics without interference in production.
  • No email storage concerns—Mailsac only counts and measures emails, we don’t store them.
  • Easy-to-use functionality that requires no prior experience with complex testing frameworks.

Getting Started with Email Load Testing

Setting up your first load test is quick and easy:

  1. Log in to your Mailsac account and navigate to the Load Testing section.
  2. Create a new load test. Assign it a name like “holiday-promo-test.”
  3. Mailsac generates a unique subdomain for your test, e.g., test-load.loadtester.msdc.co. Any email sent to this subdomain will be tracked in real time.

This subdomain lets you simulate thousands of email deliveries without polluting your production environment.


A Sample Script for Load Testing

Let’s walk through a load test. Here’s an example of how a customer might simulate email traffic using our load testing feature. This script, written in node with nodemailer, helps you generate and send thousands of emails to your test subdomain.

const nodemailer = require('nodemailer');

// SMTP Configuration
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: process.env.SMTP_PORT,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASSWORD,
  },
});

// Generate Random Emails
const generateRandomEmail = () => {
  const randomString = Math.random().toString(36).substring(2, 15);
  return `test-${randomString}@test-load.loadtester.msdc.co`;
};

// Send Email Function
const sendEmail = async (index) => {
  try {
    const mailOptions = {
      from: process.env.FROM_EMAIL,
      to: generateRandomEmail(),
      subject: `Test Email #${index}`,
      text: `This is email number ${index}`,
    };

    await transporter.sendMail(mailOptions);
    console.log(`Successfully sent email #${index}`);
  } catch (error) {
    console.error(`Failed to send email #${index}:`, error.message);
  }
};

// Run Load Test
const runLoadTest = async () => {
  const totalEmails = 500; // Number of emails to send
  for (let i = 0; i < totalEmails; i++) {
    await sendEmail(i);
  }
};

runLoadTest();

How This Script Works

  1. Generate Random Addresses: Emails are dynamically created using your test subdomain (e.g., test-load.loadtester.msdc.co).
  2. Send in Batches: The script sends emails sequentially, allowing you to control the pace of your test.
  3. Real-Time Metrics: As emails arrive, Mailsac tracks delivery rates.

Get Insights from Mailsac’s Dashboard

Once your test is running, log into the Mailsac dashboard to monitor:

  • Total emails received.
  • Delivery success rates.
  • Any failures.
  • Time-based performance analytics.

These insights help you pinpoint weaknesses in your email infrastructure, allowing you to make improvements before scaling up.


What’s Next for Your Email Testing?

Our Email Load Testing is designed to help you stress-test your email systems with ease, but it’s only the beginning. Here are some ways to take your testing further:

  1. Simulate peak loads: Experiment with higher volumes to mirror real-world events like product launches.
  2. Vary email content: Test different subject lines, body content, and attachments to ensure consistent performance.
  3. Collaborate with your team: Use shared subdomains to make testing easier across departments.

Remember, Mailsac’s Email Load Testing is designed for manual or semi-automated testing scenarios—not for full CI/CD integration. This keeps the focus on controlled, actionable insights.


With Mailsac, you can confidently validate your email systems without unnecessary complexity or expense. Ready to see it in action? Sign up today and let’s make email testing smarter, faster, and more reliable.

Will AI Replace Your Software Testing Job?


Today, we’re tackling a big question: With the rise of code generators and AI that can make UI elements based on a single drawing, will AI take your software testing job?

The short answer is no. Today we can safely tell you that no, AI won’t take your job. Instead, we think it will enhance the way you work. Let’s see how we can make AI a powerful ally for software testers.

Role of AI in Software Testing

We like to think of AI in the testing world’s context as a controlled chaos maker. We can simulate some parts of being human to introduce some unpredictability in our tests and see if our app mitigates against the chaos.

Instead of fighting AI, let’s embrace it. One of the ways we can do that is if we can have it write test cases for us, execute it, then tell us if it passed or not.

We can then easily move those generated tests to a framework like cypress or to a continuous integration environment like GitHub Actions.

So for the rest of the article, we’ll show you how you can have ai make a test case that:

  1. Generates and sends an email.
  2. Reads the contents of said email from the user’s inbox.
  3. Ensures that contents of 1 and 2 are the same.

To be safe and not risk sending any emails out to customers, we’ll use Mailsac and its API.

Making the Assistant

So for this walkthough we’re going to be using OpenAI’s “Assistant” feature. Assistants are a little bit more of a halfway point between the chat AI we all know, and an AI agent that is more autonomous.

We want to give this AI the ability to generate and run some code and read files, but not necessarily make any autonomous decisions.

For the instructions we’ll give it some system prompts to let it know under what context it should be operating under.

We’ll use the latest gpt-4o model and enable the code interpreter tools. That should allow it to read, run and execute some code for us.

Now let’s go ahead and start generating our test cases.

We’ll use the prompt

You are creating a code test that will return true of false if a certain criteria passes. The language for > all tests should be in javascript. Use dotenv for your API keys.

The criteria is:

  1. Generate an email message and send it via nodemailer using google’s SMTP servers. Send it to [email protected].
  2. Wait 2 seconds for the email to send from Google then use the mailsac API to check the subject and
    content of the email.
  3. Make the test pass if it matches what was sent in 1 and fail if it doesn’t match.

Keep in mind that you can easily tweak the SMTP to use your own service. We’ll stick with Google’s as its the most accessible for this walkthrough.

Generating the Test Case

Fire it off to the AI Assistant and let it generate the test case. In this run it generated a mocha test for me. In previous iterations it didn’t generate a whole mocha test so I’ll ask it to just use node.

As you can see it pretty much walks you through the whole project creation process. Let’s go ahead and do our setup and install.

mkdir email_test
cd email_test
npm init -y
npm install nodemailer dotenv axios

The libraries it wants to use are pretty standard. (You could make an argument against axios but hey, let’s roll with it).

And now we’ll use a .env file to hold our API keys. You’ll need an application password for the google SMTP.

You can find where to add that in your Google account.

You’ll also need a mailsac key.

The .env file is now:

MAILSAC_API_KEY=your_mailsac_api_key
GOOGLE_EMAIL=your_google_email
GOOGLE_PASSWORD=your_google_email_password

Now copy all of your keys and email onto the .env file.

Let’s start working through the code itself

Code

Looking through the code it looks fine, with 2 exceptions: It needs to be guided on the API endpoint (to use /api/text instead of /api/messages) and it the object retrived is the text itself, so we can remove the nonexistant child textBody

Now let’s go ahead and run the test.

Check on mailsac to visually make sure an email was sent.

You can even make the test fail on purpose to ensure it’s working as intended.

It works!

Extending AI

From here it’s easy to incorporate these kinds of tests into specific continuous integration frameworks of your choice.

You could use cypress and work with existing libraries or an existing framework to run your tests.

Or you could use GitHub actions to fully run your now AI generated tests in the cloud. We have a guide showing you how to do just that.

Conclusion

As you can see it still took a human element to know what to want to test, frame it, and even tweak the AI’s output a bit to make sure that what it was generating made sense.

Tester’s aren’t going anywhere anytime soon, especially if you leverage AI to do the work for you while you focus on whats important to test and why.

How You Can Avoid CC Errors That Could Violate GDPR

Developing and testing an application is difficult enough without the stress of GDPR, CAN-SPAM, accidental information leak, etc. But it doesn’t have to be.

In this article I’ll walk you through how our email platform could’ve helped prevent Running Warehouse from exposing thousands of their customer’s emails.

Background on the Running Warehouse Incident

Now, this isn’t meant to poke fun at a company’s mistake but really to highlight just how easy these mistakes are to miss. Last month, I received two emails from “Running Warehouse” about an update to their terms of service. Nothing really unusual at first glance… Until you notice that about 1000 other customer’s email addresses had been sent along in the CC field. And to add even more insult to injury, they did this TWICE. So now I have a list of about 2000 of Running Warehouse’s customers that I’m sure the customers themselves are not happy about.

Really not a good look for a company who could be facing a class action lawsuit for a previous data breach back in 2021. 

So let’s look at the implications of this easy to do mistake:

  1. Potential GDPR violations
  2. Loss of customer trust
  3. Company reputation lost via social media, youtube videos about you, forums, etc.

Prevent these errors with Mailsac

Obviously no company wants that! This is where mailsac steps in. We help prevent these kinds of easy mistakes, but at the application level. We have a set of features where you can hook up your continuous integration with our API to ensure the email you want to send is the one the recipient actually received.

So let’s walk through how we’d prevent this if this email was coming from our application.

Technical Deep-Dive

Here’s what we’ll do:

  1. Setup a sample application that is meant to simulate sending emails.
  2. Setup our Mailsac API Key.
  3. Send a sample email.
  4. Use the API to ensure our cc and email fields match what we sent.

Let’s get started.

Sample Application

The sample application we'll be using
  • To stand in place of your application, we’ll use this dead simple node app whose sole purpose is to just send an email based on what you place in a form. 
  • To simulate our email service API key, we’ll use our Application Key we generated for our gmail account. You can do this too if you want to step through this part. You can find it under your Google account -> security -> 2 FA -> App Password or just search for app passwords.
  • Add your credentials in the config.js file directly.
    • Again, while we embed the code here, you should really use a dot-env library.
  • Most importantly, for our example here, let’s use a mailsac temporary address. We have ad hoc inbox creation abilities so for now, let’s say we’ll send it to [email protected]
  • Fire up the app

Setup our Mailsac API Key

Before we fire off an email, let’s add some quick code to check whether what we send is the same or not once it’s received.

Mailsac API Dashboard

The Email CC Checker Snippet

  await setTimeout( async () => {
        const mailsac = new Mailsac({ headers: { "Mailsac-Key":  process.env.MAILSAC_API } })
        const results = await mailsac.messages.listMessages("[email protected]")
        const messages = results.data;
        if (messages[0].cc.length <= 0){
            console.error("This email has no CC. That's Good!")
        } 
        if (messages[0].subject === req.body.subject){
            console.error("This has the correct subject. That's Good!")
        }
      }, 1000);

Normally we’d place it in an .env file, but for our purposes here, we’ll just place it directly in the headers.

Send Sample Email

Alright now let’s fire this up and fire off an email

  • Let’s make the content and subject something specific like:
    • Subject: This email is intended only for emailcontentcheck
    • Content: Hey, this email is strictly for [email protected]. No others on cc field.
  • Fire it off
  • Wait for the check to fire off and print out comparison / success
Showing a successful email cc check.
Showing a successful email cc check.

Use the API to ensure our cc and email fields match what we sent

Right after our code fires off, it should print out the message comparison result. But now, let’s make sure our CC fields are also empty

      await setTimeout( async () => {
        const mailsac = new Mailsac({ headers: { "Mailsac-Key": process.env.MAILSAC_KEY } })
        const results = await mailsac.messages.listMessages("[email protected]")
        const messages = results.data;
        if (messages[0].cc.length &lt;= 0){
            console.error("This email has no CC. That's Good!")
        } 
        if (messages[0].subject === req.body.subject){
            console.error("This has the correct subject. That's Good!")
        }
      }, 1000);

We’re even throwing in a subject line check. You can check out the full API documentation to see how you can check for empty BCCs, content checks, and more.

Fire it off again and enjoy having an automated way test your email content, cc fields, and more!

Wrap Up

Finding the right words to use in an email is already hard enough, don’t complicate it by worrying about whether the email went through,  if it works right, if it accidentally cc’s people, etc. 

Just let mailsac handle the double checking for you.

And if you want to dive even deeper into email testing, we have a full set of articles on integrating with Cypress or integrating other testing tools like Selenium with GitHub Actions for a fully automated testing pipeline.

The Cost of Email Mistakes: 5 Case Studies You Need to Know

The Cost of Email Mistakes: 5 Case Studies You Need to Know

Email is a crucial communication tool for businesses, even in 2024. It’s simple and effective. But in its simplicity we can find ourselves complacent in how we use it. Or even who we send our emails to.

In this article, we’ll examine five real-life case studies where emails went wrong and caused significant problems for the organization that sent it. From exposing private information to damaging a company’s reputation, these examples highlight the potential risks of email errors. By understanding these incidents, businesses can learn the importance of proper email management and take steps to prevent similar mistakes. These case studies serve as a reminder that attention to detail is essential in all aspects of communication.

Case Study #1 – HBO Max’s “Integration Test Email” Incident

HBO says: "Yes, it was the intern"

In June 2021, HBO Max accidentally sent an “Integration Test Email #1” to a large number of its subscribers. As you can guess, the email was meant for internal testing but mistakenly went out to real customers. The incident caused widespread confusion and snark on social media. Some people pointed out the optics of mistakenly sending out an email to your entire client base, but most took the email fail in jest:

Tweet of someone that reads: "For the #1 Integration Test Email in ur life <3 @hbomax"

Hilariously, the official HBO Max twitter account had this explanation:

Tweet that reads: "We mistakenly sent out an empty test email to a portion of our HBO Max mailing list this evening. We apologize for the inconvenience, and as the jokes pile in, yes, it was the intern. No, really. And we’re helping them through it. ❤️"

We don’t blame the intern, but we do cast some judgment on their complete lack of email service testing! Using disposable email addresses for internal testing could have prevented this. But hey, we did at least get some genuine entertainment.  

Case Study 2 – Running Warehouse CC vs BCC

Header that reads "Running Warehouse vs BCC and CC"

Sadly this case is not as harmless. On May 2024 RunningWarehouse, a discount shoe seller, sent out what should have been a routine email about updating their terms of service.

As you can see, this wasn’t just sent to a single customer. While not the entire customer list, approximately 999 customers is pretty indicative of an email testing batching failure. And it didn’t go unnoticed either, as there was at least one 30 post thread on arguably the internet’s largest running forum. 

The issue is still relatively fresh as of this writing, but it’s safe to assume that Running Warehouse did not intend to leak 1000 of its customer’s email addresses to each other due to someone mistaking CC vs BCC. And even more seriously, it could even be a violation of GDPR under “Sharing Email Addresses Without Consent” according to GDPR’s Data Breach Guidelines.

Case Study 3 – University Backtracks Acceptance Letters

Banner image that reads "Sorry, Wrong Recipient"

In January 2019, the University of South Florida St. Petersburg mistakenly sent acceptance emails to nearly 680 applicants, although only 250 had been accepted. As you can imagine this caused confusion, disappointment, and reputational damage to the university. The affected students and their families were left feeling misled, and the university had to invest considerable time and resources to address the fallout from the erroneous communication. 

You may not be shocked to hear that this isn’t the first time this has happened, as Columbia University also accidentally sent 270 emails to students who had not been officially accepted. 

These cases are a clear cut impact of email errors on institutional credibility and the importance of systematic verification before sending mass emails. Systems sending out emails absolutely need to ensure that the emails they intend to send are 100% accurate.

Case Study 4 – Australian Leaks Citizen Emails

Banner text that reads "Australia vs BCC and CC"

In September 2020, an employee at Australia’s Department of Foreign Affairs and Trade (DFAT) accidentally exposed the personal details of almost 3,000 citizens by not using the BCC field in an email. This revealed the email addresses of Australians stranded abroad due to COVID-19 travel restrictions. The mishandling of this sensitive information led to major privacy concerns and required immediate action, such as attempting to recall the email and asking recipients to delete it. 

While not directly tied to a system, temporary email accounts could be used in place to ensure the intended audience and message gets to its audience intact. 

Case Study 5 – Serco Accidentally Shares Contract Tracers

Banner text that reads: "Serco vs BCC and CC"

In May 2020, Serco, a business services and outsourcing company, accidentally exposed the email addresses of nearly 300 newly recruited COVID-19 contact tracers by using the CC field instead of BCC. This breach of privacy led to concerns about the security of personal data and put the company’s data protection practices under scrutiny. Serco apologized and announced that they would review and update their procedures to prevent similar incidents in the future. 

Yet another case of manual implementations of email lists without temporary email addresses, email outbound trapping services, or message integrity verification (ie, only 1 person in the to field, nothing in the cc field).

Stop Customer Data Leaks & Company Reputation Hits With Mailsac

We offer several benefits that can help prevent incidents like the ones described above:

  • Enhanced Security: Temporary emails protect more than just attacks and spam. By using a disposable email address, you can send test emails either manually or via your system to ensure the message arrives exactly as intended.
  • Privacy Protection: Mailsac helps keep personal and sensitive email addresses private. This is especially useful for companies conducting internal tests or handling sensitive information. 
  • Minimized Clutter: Using temporary emails for one-time use reduces inbox clutter around testing. It’s beneficial for both individuals and teams, as it keeps testing cycles organized. You can even split your testing into campaigns by using unique email addresses you control with every testing cycle with our Zero-Setup subdomains feature.
  • Testing and Development: Mailsac is ideal for testing campaigns, systems, and internal communications. Temporary emails can be used during development phases to catch errors and ensure that only verified emails are sent to real users. We have fully featured APIs to integrate between any systems.

Disposable email services are an essential tool for modern communication. They can help prevent costly mistakes and ensure that PR issues around email remain in the past. For those looking to enhance their email security and efficiency, we’re an excellent choice. Give Mailsac a try today and see how it can benefit your email needs.

Have Playwright Automatically Write Tests For You with Codegen

Related: test email OTP codes and magic links in Playwright.

Testing signup or password-reset emails? See Playwright email testing with Mailsac: a tested helper that waits for the right email, reads its code or link, and runs in parallel workers and GitHub Actions.

These days, we have better options than writing our test specs by hand.

The open source community has released a variety of frameworks to relieve us from that particular tedium: Cypress, Selenium and Pupeteer. And in this video companion guide, I’ll focus on Playwright. Specifically, how playwright can help automate a lot of the boilerplate involved in writing test specs with CodePen.

The video will do a quick walk you through playwright in itself. This article will provide the core login.spec.ts file I used and where to go from next.

But before we do that, it’s worth mentioning that efficient testing isn’t just about the right tools; it’s also about the seamless integration of these tools into your existing systems. That’s where Mailsac comes in. Our platform offers unique capabilities for email testing within your automated workflows, making it an excellent complement to Playwright for end-to-end testing solutions.

With Mailsac, you can ensure not just the functionality but also the integrity of email interactions in your applications, all within the automation framework you’ll establish with Playwright.

So, What Is Playwright?

Playwright is an open-source automation library created by Microsoft. It’s designed to enable developers and testers to write reliable and efficient tests for web applications.

It’s cross platform and officially compatible with the major browsers.

Diving Into Codegen

Start by installing playwright inside your project

npm init playwright@latest

I’ll be using the defaults for this guide. After installation you’ll have these files generated in your project:

Files generated
playwright.config.ts
package.json
package-lock.json
tests/
example.spec.ts
tests-examples/
demo-todo-app.spec.ts
For more: https://playwright.dev/docs/test-configuration

Putting Codegen Through Its Paces

First Run

Fire up codegen via the built in console

npx playwright codegen

As you click around your application you’ll see codegen record each click based on its css class.

It will build each line as you go about your test. In our video, we perform a login with an incorrect set of credentials and a correct set.

Our login.spec.ts file

Before our manual edits, this is what codegen generated for us:

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.getByLabel('Email Address').click();
  await page.locator('form div').filter({ hasText: 'Email AddressEmail Address' }).getByRole('paragraph').click();
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill('[email protected]');
  await page.getByLabel('Email Address').press('Tab');
  await page.getByLabel('Password').fill('password');
  await page.getByLabel('Password').press('Enter');
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill('[email protected]');
  await page.getByLabel('Email Address').press('Tab');
  await page.getByLabel('Password').fill('password123');
  await page.getByLabel('Password').press('Enter');
  await page.getByRole('button', { name: 'Settings' }).click();
  await page.getByText('Note: Your email mailsac.demo').click({
    button: 'middle'
  });
  await page.getByRole('button', { name: 'Next boilerplate' }).click();
});

All we had to do was manually add was the highlighted lines (13 and 19) to turn it into a real test:

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.getByLabel('Email Address').click();
  await page.locator('form div').filter({ hasText: 'Email AddressEmail Address' }).getByRole('paragraph').click();
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill('[email protected]');
  await page.getByLabel('Email Address').press('Tab');
  await page.getByLabel('Password').fill('password');
  await page.getByLabel('Password').press('Enter');
  await expect(page).toHaveURL('http://localhost:3000/login');
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill('[email protected]');
  await page.getByLabel('Email Address').press('Tab');
  await page.getByLabel('Password').fill('password123');
  await page.getByLabel('Password').press('Enter');
  await expect(page).toHaveURL('http://localhost:3000/home');
  await page.getByRole('button', { name: 'Settings' }).click();
  await page.getByText('Note: Your email mailsac.demo').click({
    button: 'middle'
  });
  await page.getByRole('button', { name: 'Next boilerplate' }).click();
});

Running the Test

Running the test by default shows no visual progress. But if you’d like to see the browser run through your steps visually, you’ll need to issue the command:

npx playwright test --headed

Where to go next

The most natural next step is integrating playwright tests with a continuous integration platform like Travis or Github Actions. Plugging playwright into a CI system like Github Actions is fully supprted by playwright natively.

Another possible progression is using playwright to test critical paths in your application like user registration or password reset flows. We have a full guide on how to do that with another framework, Cypress.

If you want us to explore how you can integrate playwright with email testing and Github Actions or any other potential playwright integrations, let us know on our forums. We’ve only scratched the very surface of what playwright can do.

Until next time.

Cypress Email Testing: Check Reset Links, Verification Emails and OTP Codes with Mailsac

Updated September 23, 2026 by Mailsac Engineering. Examples use Cypress 16 and @mailsac/cypress 0.1.0.

New: the complete Cypress email testing guide covers signup codes, password reset links, magic links, GitHub Actions and parallel runs, with tested examples.

In short: Cypress can’t read email by itself. Mailsac gives your tests real inboxes at mailsac.com or your own test domain, and the @mailsac/cypress plugin waits for the email your test just triggered, then returns its reset link or verification code. The API key stays in Cypress’s Node process, never in the browser. New to this? See how the email testing API works.

Get a free API key → The free plan includes an API key, 1 private address and 1,500 operations a month. Team plans with shared custom domains start at $89/month.

Cypress email testing can cover the whole password-reset journey: request an email, receive it, open the correct link, set a new password, and sign in. The difficult part is making sure your test reads the message it just requested, rather than an older reset email or an unrelated notification.

The Mailsac Cypress integration handles that wait in Cypress’s Node process. Your application sends mail through its usual email provider; Mailsac receives it, and the integration reads the matching message through the Mailsac API. The API key stays out of the browser.

Start with the runnable example below, then connect the same pattern to your application. For real delivery, create a Mailsac account and use a test inbox whose message bodies your API key can read.

Run a complete password-reset test first

Use Node.js 22 or 24, the versions exercised in the package’s CI. Clone the published release and run:

git clone --branch v0.1.0 https://github.com/mailsac/cypress-mailsac.git
cd cypress-mailsac
npm ci
npm run test:e2e

The runner starts a small example app and a local Mailsac API fixture, runs Cypress headlessly, then stops the app. This default run needs no account, API key, or SMTP credentials. It sends no email and does not test delivery to the real Mailsac service.

The fixture deliberately contains an old password-reset email and a newer unrelated message. The correct email arrives after a delay. The test waits for it, follows its reset link, changes the password, verifies that the old password fails and the new password works, and checks that the reset link cannot be reused.

The example’s README also documents a separate SMTP mode. That mode sends a real message through your SMTP provider to a Mailsac inbox you control. Copy its .env.smtp.example to the ignored .env.smtp file, supply your SMTP settings and Mailsac key, then follow the two-terminal instructions. The app is a learning example with in-memory accounts, not a production authentication service.

Add Mailsac to your Cypress project

For the versions used in this guide:

npm install --save-dev cypress@16.1.0 @mailsac/cypress@0.1.0

Set MAILSAC_API_KEY in your local process environment or CI secret store. Do not prefix it with CYPRESS_, put it in Cypress.env() or config.env, or commit it.

Register the Node tasks in cypress.config.js. This example uses CommonJS configuration; merge these settings into your existing configuration and adjust baseUrl to your running app.

const { defineConfig } = require('cypress');
const { createMailsacTasks } = require('@mailsac/cypress/node');

module.exports = defineConfig({
  video: false,
  screenshotOnRunFailure: false,
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
      on('task', createMailsacTasks());
      return config;
    },
  },
});

Then add the command registration to cypress/support/e2e.js or cypress/support/e2e.ts:

import '@mailsac/cypress/commands';

The command passes message criteria to a Node task; only the resulting message data comes back to the test. Automatic failure screenshots and video are disabled here because password-reset tokens appear in URLs. Also avoid logging message bodies, links, or codes.

Wait for the right email and follow its reset link

Save this spec as cypress/e2e/password-reset.cy.js. It uses the routes, button labels, and subject from the example app. When applying it to your own app, change the inbox, subject, reset path, selectors, and assertions to match your implementation. Start your app before running Cypress and configure it to send to the chosen test inbox.

import { extractLink } from '@mailsac/cypress';

it('resets a password and signs in', () => {
  const email = 'your-private-test-inbox@mailsac.com';
  const newPassword = 'my-new-demo-password';
  let receivedAfter;

  cy.visit('/forgot-password');
  cy.get('input[name=email]').type(email);

  // Capture immediately before requesting the email.
  cy.then(() => { receivedAfter = new Date().toISOString(); });
  cy.contains('button', 'Send reset email').click();

  cy.then(() => cy.mailsacWaitForMessage({
    email,
    receivedAfter,
    subject: 'Reset your Example App password',
    timeoutMs: 60_000,
    pollIntervalMs: 1_000,
  })).then((message) => {
    const resetUrl = extractLink(message, {
      origin: new URL(Cypress.config('baseUrl')).origin,
      pathname: '/reset',
    });
    cy.visit(resetUrl, { log: false });
  });

  cy.get('input[name=password]').type(newPassword, { log: false });
  cy.contains('button', 'Update password').click();
  cy.contains('h1', 'Password updated').should('be.visible');
  cy.contains('a', 'Sign in').click();
  cy.get('input[name=email]').type(email);
  cy.get('input[name=password]').type(newPassword, { log: false });
  cy.contains('button', 'Sign in').click();
  cy.get('[role=status]').should('contain', 'signed in successfully');
});

With your app running and the test adapted to it, run:

npx cypress run --spec cypress/e2e/password-reset.cy.js

receivedAfter excludes older mail. Capturing it before the click also prevents a fast-arriving message from being excluded. An exact subject narrows the result; you can additionally match a sender with from, or use subjectIncludes for a subject substring. All supplied filters must match.

extractLink requires one distinct link with your app’s exact origin and reset path. It fails if the email has no matching link or several different matching links. That keeps a test from accidentally following a footer, help page, or unrelated website. If your sender wraps links in a tracking URL, disable tracking for these test messages or explicitly test that redirect flow.

Keep email tests reliable in CI

  • Bound the wait. The default is 30 seconds; the example allows 60. The maximum is 120 seconds, including requests and retries. A timeout should fail the test rather than wait indefinitely.
  • Separate parallel tests. Use a distinct inbox or a unique subject per test. A timestamp alone cannot distinguish simultaneous requests to one inbox. Keep the runner’s clock synchronized.
  • Use dedicated inboxes. Each poll checks the latest 100 messages. Avoid a busy shared catch-all. Polls and message reads consume Mailsac API operations; choose your interval and concurrency accordingly.
  • Protect reset links. Use a private inbox or owned domain for sensitive messages. The integration reads mail; it does not delete messages or change retention settings.

To run the cloned repository’s fixture example in GitHub Actions, no service secrets are needed:

name: Email test example
on: [push, pull_request]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test:e2e

For your own app’s live-delivery CI test, start the app and its mail sender, provide MAILSAC_API_KEY through the CI secret store, and run your adapted Cypress spec. Keep that explicit live test separate from the local fixture run.

Testing verification codes too

For an OTP flow, use extractCode(message) on the matching message to return one distinct standalone six-digit code, then enter it with { log: false }. The helper rejects missing or ambiguous codes and supports custom patterns. See the OTP example and API reference.

Cypress email testing FAQ

Can Cypress read emails?

Not on its own: Cypress drives the browser, and email arrives outside it. A Cypress task running in Node can call an inbox API, though. That’s what @mailsac/cypress does: cy.mailsacWaitForMessage() polls Mailsac from Node until the matching message arrives and returns its content to your test.

How do I test email OTP or verification codes in Cypress?

Wait for the message as in the reset example, then call extractCode(message) and type the result with { log: false }. It returns one distinct six-digit code by default and fails if the code is missing or ambiguous; you can pass a custom pattern for other formats.

Do I need my own email server or domain?

No. Any @mailsac.com address can receive mail without setup, which is fine for made-up test accounts. Public inboxes are readable by anyone, so for reset links and codes on realistic accounts use a private address or a custom domain. Mailsac can host a test subdomain for you, with no DNS changes needed.

Will this work in CI with parallel runs?

Yes. Give each test its own address (or a unique subject), capture receivedAfter just before triggering the email, and supply MAILSAC_API_KEY from your CI secret store. Each poll and message read counts as an API operation, so set the poll interval to match your plan.

The original 2023 video remains available for historical reference. It predates this integration; use the package and code above for the current workflow.

Create your Mailsac account, install the Cypress integration, or explore the complete password-reset example.

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.

Looking for up-to-date test code? Our Selenium email testing guide has a signup verification test for Node.js and headless Chrome. It uses a unique address per run, waits only for new mail, and comes with a ready-to-use GitHub Actions workflow. This post walks through the GitHub Actions setup step by step.

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/selenoid-github-action@v2

    - uses: actions/checkout@v1
    - 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/selenoid-github-action@v2

    - uses: actions/checkout@v1
    - 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.