JavaScript Testing

2025 Jest/Puppeteer Helper Import: The Ultimate Guide

Tired of boilerplate? Master the 2025 way to import Jest & Puppeteer helpers. This guide simplifies your E2E testing setup for cleaner, faster, and more maintainable code.

A

Alex Miller

Senior Full-Stack Engineer specializing in test automation, CI/CD, and modern JavaScript frameworks.

6 min read1 views

2025 Jest/Puppeteer Helper Import: The Ultimate Guide

Does your Jest/Puppeteer test file look like a mile-long setup script before you even get to the first it block? You spend ages writing boilerplate to launch a browser, open a new page, and handle cleanup, all before you can test a single button click. You’re not alone. For years, this has been a common frustration in end-to-end (E2E) testing.

But it’s 2025, and it’s time to leave that mess behind. There’s a cleaner, more modular, and infinitely more maintainable way to structure your tests. This guide will walk you through the ultimate approach to importing and managing Jest and Puppeteer helpers, transforming your testing workflow from a chore into a charm.

The Problem with the “Old Way”

Let’s be honest. You’ve probably written code that looks something like this inside your test file:

// in some-feature.test.js
const puppeteer = require('puppeteer');

describe('Some Feature', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch();
    page = await browser.newPage();
    jest.setTimeout(30000); // Set a long timeout
  });

  beforeEach(async () => {
    await page.goto('https://example.com/login');
  });

  afterAll(async () => {
    await browser.close();
  });

  it('should do something', async () => {
    // ... actual test logic
  });
});

What’s wrong with this? For a single file, maybe nothing. But multiply this across ten, twenty, or a hundred test files. You’re facing:

  • Massive Code Duplication: The same beforeAll and afterAll blocks are copied everywhere.
  • Maintenance Headaches: Need to change the Puppeteer launch options (e.g., add --no-sandbox or run in headless mode)? You have to update every single test file.
  • Poor Readability: Test files are cluttered with setup logic, obscuring the actual tests they are supposed to run.

The 2025 Philosophy: Centralized & Reusable Helpers

The modern approach embraces the Don't Repeat Yourself (DRY) principle. We'll centralize two types of logic:

  1. Lifecycle Management: The code that launches and closes the browser and page.
  2. Utility Functions: Reusable actions you perform all the time, like logging in, taking a screenshot, or waiting for a specific element.

By creating dedicated helper modules, we can import just what we need, keeping our test files lean and focused on their specific purpose.

Step-by-Step: Building Your Helper Module

Let's build this from the ground up. It’s simpler than you think.

Step 1: The Project Structure

A clean structure is the foundation. Inside your tests directory (commonly __tests__ or tests), create a `helpers` folder.

my-project/
├── __tests__/
│   ├── helpers/
│   │   ├── puppeteer.js   // Handles browser/page lifecycle
│   │   └── utils.js         // Contains common reusable functions
│   ├── login.test.js
│   └── dashboard.test.js
├── jest.config.js
└── package.json

Step 2: Create the Core Puppeteer Lifecycle Helper

This is the most important part. We'll create a file that manages the browser instance for us. This file will use Jest's beforeAll and afterAll hooks and export the `browser` and `page` objects.

Create __tests__/helpers/puppeteer.js:

// __tests__/helpers/puppeteer.js

const puppeteer = require('puppeteer');

// We can set a single timeout for all tests.
// No more repeating this in every file!
jest.setTimeout(30000);

let browser;
let page;

// This runs once before all tests in a suite.
// It launches the browser and creates a new page.
beforeAll(async () => {
  browser = await puppeteer.launch({
    headless: 'new', // Use the new headless mode
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  });
  page = await browser.newPage();
});

// This runs once after all tests in a suite.
// It closes the browser.
afterAll(async () => {
  if (browser) {
    await browser.close();
  }
});

// Export the browser and page objects so they can be used in tests.
// We wrap them in a function to ensure they are initialized before being accessed.
module.exports = { 
  getBrowser: () => browser, 
  getPage: () => page, 
};

Why wrap exports in functions? This prevents issues where a test file might try to access `browser` or `page` before beforeAll has finished running. Calling `getPage()` ensures you get the initialized instance.

Step 3: Create Reusable Utility Functions

Now, let's create helpers for common actions. These functions will accept the `page` object as their first argument.

Create __tests__/helpers/utils.js:

// __tests__/helpers/utils.js

/**
 * A helper function to log in a user.
 * @param {import('puppeteer').Page} page - The Puppeteer page object.
 * @param {string} username - The username to enter.
 * @param {string} password - The password to enter.
 */
async function login(page, username, password) {
  await page.goto('https://yourapp.com/login');
  await page.type('#username', username);
  await page.type('#password', password);
  await page.click('button[type="submit"]');
  await page.waitForNavigation();
}

/**
 * Clicks a selector and waits for navigation.
 * @param {import('puppeteer').Page} page - The Puppeteer page object.
 * @param {string} selector - The CSS selector to click.
 */
async function clickAndWait(page, selector) {
  await Promise.all([
    page.waitForNavigation(),
    page.click(selector),
  ]);
}

/**
 * Takes a screenshot with a standardized name.
 * @param {import('puppeteer').Page} page - The Puppeteer page object.
 * @param {string} name - The name for the screenshot file.
 */
async function takeScreenshot(page, name) {
  await page.screenshot({ path: `screenshots/${name}-${Date.now()}.png` });
}

module.exports = { login, clickAndWait, takeScreenshot };

Look how clean that is! Each function has a single responsibility. Adding a new helper, like `getText(page, selector)`, is trivial.

Putting It All Together: The Clean Test File

Now for the magic. Let's rewrite our `login.test.js` to use these new helpers. The difference is night and day.

Your new __tests__/login.test.js:

// __tests__/login.test.js

// Import our lifecycle helper. This single line handles all setup and teardown!
const { getPage } = require('./helpers/puppeteer'); 

// Import the specific utility functions we need.
const { login } = require('./helpers/utils');

describe('Login Flow', () => {
  it('should allow a valid user to log in successfully', async () => {
    // Get the initialized page object from our helper
    const page = getPage();

    // Use our clean, reusable login function
    await login(page, 'testuser', 'strongpassword123');

    // Assert the outcome
    const heading = await page.$eval('h1', el => el.textContent);
    expect(heading).toBe('Welcome, testuser!');
  });

  it('should show an error message with invalid credentials', async () => {
    const page = getPage();

    await login(page, 'invalid-user', 'wrong-password');

    const errorMessage = await page.$eval('.error-message', el => el.textContent);
    expect(errorMessage).toContain('Invalid username or password');
  });
});

Compare this to the original. It’s beautiful! The test file is now 100% focused on what to test, not how to set it up. It's readable, declarative, and incredibly easy to maintain.

Advanced Tip: Explicit Imports vs. Global Setup

You might have heard of Jest's `globalSetup` and `globalTeardown` configurations. These allow you to run a script once before and after *all* your test suites.

  • Pros of Global Setup: Can be faster, as the browser is launched only once for the entire test run.
  • Cons of Global Setup: It hides dependencies. A test file might magically work without you knowing why. It also makes it harder to run a single test file in isolation and can lead to state bleeding between test files if not managed carefully.

My Recommendation for 2025: Stick with the explicit, per-suite import approach shown in this guide. The clarity, test isolation, and maintainability it provides far outweigh the minor performance gain of a global setup for most projects. It forces a clear dependency chain and makes your test suites self-contained and robust.

Conclusion: Test Smarter, Not Harder

By moving your Puppeteer lifecycle logic and common actions into dedicated helper modules, you achieve a level of organization that pays dividends immediately. Your tests become:

  • Readable: They describe business logic, not setup procedures.
  • Maintainable: Update a launch option or a login selector in one place.
  • Scalable: Adding new tests and helpers is simple and clean.

Stop fighting boilerplate and start writing E2E tests that are a pleasure to work with. Adopt this helper import pattern and spend your time building reliable, high-quality applications.