Most interactions with a web application occur at the browser. Users search items, fill forms, create shopping carts, log into their profiles, and perform many other tasks. Unit tests are great, but testing an application by interacting with the user-facing frontend is also a must.

In this tutorial, you will learn how to write tests for the browser using Puppeteer. You will then take this a step further by automating the testing process in a continuous integration flow.

Prerequisites

To follow this tutorial, a few things are required:

  1. Basic knowledge of JavaScript
  2. Node.js installed on your system (version >= 12)
  3. A CircleCI account
  4. A GitHub account

With all these installed and set up, you can begin the tutorial.

Cloning and running the sample application

The first step is to set up a web application to test. You will need to clone a demo application by running this command:

git clone --single-branch --branch base-project https://github.com/CIRCLECI-GWP/functional-testing-puppeteer

Then, go to the root of the project (cd functional-testing-puppeteer) and install dependencies by running:

npm install

With the dependencies fully installed, run the application using this command:

node server

The application server will boot up at http://localhost:5000. Go to that URL on your browser to review the demo app homepage.

Homepage - demo app

The demo page consists of a header that reads Welcome to the demo Web Page, a paragraph that contains the text This is a sample text in a paragraph on the page, and a form with an email field and submit button. When filled and submitted, the form prints the submitted email to the screen.

Fill form - demo app

Installing Jest and Puppeteer

You will be testing the demo page to confirm that it contains the elements displayed on the page, and to verify the behavior of the form. You will need two packages:

  • Puppeteer is the browser testing framework.
  • Jest serves as the test runner for the test suites that you will write.

Install these packages as development dependencies using this command:

npm install --save-dev puppeteer jest

With these installed, you can now begin adding browser tests to the project.

Adding tests using Puppeteer

The first elements you will be testing are the header and the paragraph. The tests will confirm that these elements are on the page and contain the expected text within them.

Add a new test file named homepage.test.js at the root of the project. Add this code:

const puppeteer = require("puppeteer");

test("Confirm text on page", async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();

    await page.goto("http://localhost:5000");

    let pageHeader = await page.$("#pageTitle");
    let pageHeaderValue = await pageHeader.evaluate((el) => el.textContent);

    expect(pageHeaderValue).toContain("Welcome to the demo Web Page");

    let pageParagraph = await page.$("#pageParagraph");
    let pageParagraphValue = await pageParagraph.evaluate((el) => el.textContent);

    expect(pageParagraphValue).toContain("This is a sample text in a paragraph on the page");
  } finally {
    await browser.close();
  }
}, 120000);

This test case launches a new browser instance and loads the web app at http://localhost:5000. It then uses CSS selectors to target both the header and paragraph to read their contents. The contents of the header and paragraph are then compared against the expected result.

To run these tests, modify the test script in the package.json using this code:

...
"scripts" : {
  "test" : "jest"
}

Make sure that the web app is running (using node server). Then run this command:

npm run test

After this runs, the CLI output shows that you have tests that passed.

 PASS  ./homepage.test.js
  ✓ Confirm text on page (3344 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.821 s
Ran all test suites.

Next, add another test case to test the form behavior. Add this code below the first test case:

...
test("Confirm form submission output", async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();

    await page.goto("http://localhost:5000");

    await page.type("#userEmail", "test@company.com");
    await page.click("#submitButton");

    let emailContainer = await page.$("#infoDisplay");
    let value = await emailContainer.evaluate((el) => el.textContent);

    expect(value).toContain("test@company.com");
  } finally {
    await browser.close();
  }
}, 120000);

The second test case references the email field using its id attribute and the email (test@company.com) that was entered into the field. The test references the submit button and “clicks” it using the button’s id attribute. Finally, the display container for the email is targeted and the text content is fetched and compared to the expected value, which is the email that was entered.

Save this file and then run your tests again using the npm run test command. The CLI output shows that the tests have passed.

 PASS  ./homepage.test.js
  ✓ Confirm text on page (1150 ms)
  ✓ Confirm form submission output (577 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.187 s, estimated 4 s
Ran all test suites.

Setting up a CI/CD pipeline to automate testing

To complete the goal of this tutorial, we will automate the testing process by plugging it into a CI/CD pipeline. The pipeline runs the tests each time the code is updated on your remote repository.

To begin, create a folder named .circleci at the root of the project. Add a configuration file named config.yml inside the folder you just created. In this file, enter the following code:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:21.7.0-browsers
    steps:
      - checkout
      - run:
          name: Update NPM
          command: "sudo npm install -g npm"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: Install Dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Run the application
          command: node server.js
          background: true
      - run:
          name: Run tests
          command: npm run test

In this configuration, the required Node.js image is pulled in. This image contains browsers, which is important for running the browser tests. The next part of the config updates npm. The project dependencies are then installed and cached.

To run the browser tests against the application, the application is booted up in a background process using node server. The tests are then run using the npm run test command. Once you are done, push the code to GitHub. Make sure that this is the GitHub account connected to your CircleCI account.

Next, go to the Projects page on the CircleCI dashboard to add the project.

Add project - CircleCI

Click Set Up Project to open the Setup page. Enter the name of the branch your code is on in GitHub. Then click Set Up Project when prompted.

Add config - CircleCI

This will automatically trigger the pipeline.

Build successful - CircleCI

Success! Click build to review the details of the test.

Build details - CircleCI

Great work!

Conclusion

Testing from the perspective of a user of the application gives developers so much insight. You can learn how the different units of the application work together to help the user achieve their tasks in your applications. While most interface/browser tests are still completed manually, in this tutorial, you have learned how to automate these types of tests to speed up development flow and increase productivity.

Functional browser testing improves the quality of your application by giving your team the ability to quickly catch and respond to bugs. Be sure to share the details of your successful tests with your team so that they can also benefit.

Happy coding!


Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.

Read more posts by Fikayo Adepoju