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 nothing beats testing an application by interacting with the user-facing frontend. In this tutorial, you will learn and demonstrate 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, we 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/browser-testing

Then, go to the root of the project (cd browser-testing) 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 (32.732 s)
  ✓ Confirm text on page (25962 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        41.205 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 (64.297 s)
  ✓ Confirm text on page (42118 ms)
  ✓ Confirm form submission output (9664 ms)

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

Setting up a CI pipeline to automate testing

To complete the goal of this tutorial, we will automate the testing process by plugging it into a continuous integration (CI) pipeline. The pipeline runs the tests each time the code is updated on your remote repository. The first step is to 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 the Set Up Project button to open the Setup page. Then click Skip this step on the modal that pops up because we will be manually adding our CircleCI config later in this tutorial.

Add Config - CircleCI

On the Setup page, click Use Existing Config to indicate that you will be adding a configuration file manually and not using the sample. Next, you are prompted to either download a configuration file for the pipeline or start building.

Build Prompt - CircleCI

Click Start Building. This build will fail because we have not set up our configuration file yet. Setting it up is our next step.

For the final step, 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: circleci/node:12-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.

Commit all changes to the project and push to your remote GitHub repository. 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 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. Despite that, most interface/browser tests are still completed manually. In this tutorial, you have demonstrated 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 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 from this information.

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.