Skip to main content

JavaScript E2E Testing

Introduction

End-to-end (E2E) testing is a methodology used to test an application's workflow from beginning to end. Unlike unit or integration tests that focus on specific components or interactions between components, E2E tests verify that the entire application works as expected from the user's perspective.

In JavaScript applications, E2E testing helps ensure that all the parts of your application—frontend, backend, database, and external services—work together correctly. This type of testing is crucial because it catches issues that might not be apparent when testing components in isolation.

What is E2E Testing?

End-to-end testing simulates real user scenarios by:

  1. Opening a browser
  2. Navigating to your application
  3. Interacting with elements on the page
  4. Verifying expected outcomes

Think of E2E testing as having a robot that acts as a user, performing tasks and checking if everything works correctly.

Several tools are available for E2E testing in JavaScript applications:

  1. Cypress - Modern, developer-friendly testing tool
  2. Playwright - Cross-browser automation library by Microsoft
  3. Selenium WebDriver - Traditional, widely-used automation tool
  4. Puppeteer - Headless Chrome Node.js API
  5. TestCafe - No-configuration testing solution

For this tutorial, we'll focus on Cypress, as it's beginner-friendly and widely adopted.

Setting Up Cypress for E2E Testing

Let's start by setting up Cypress in a JavaScript project.

Installation

First, install Cypress as a dev dependency:

bash
npm install cypress --save-dev

Configuration

Add a script to your package.json to open Cypress:

json
{
"scripts": {
"cypress:open": "cypress open"
}
}

Run Cypress for the first time:

bash
npm run cypress:open

This creates a cypress folder in your project with the following structure:

cypress/
├── e2e/ # Your test files go here
├── fixtures/ # Test data files
├── support/ # Helper functions and commands
└── videos/ # Recordings of your test runs

Writing Your First E2E Test

Let's write a simple test that verifies a login flow. Create a file called login.cy.js in the cypress/e2e directory:

javascript
describe('Login Flow', () => {
beforeEach(() => {
// Visit the login page before each test
cy.visit('/login');
});

it('should login with correct credentials', () => {
// Type in the username and password
cy.get('#username').type('testuser');
cy.get('#password').type('password123');

// Click the login button
cy.get('#loginButton').click();

// Verify that we're redirected to the dashboard
cy.url().should('include', '/dashboard');

// Verify that the welcome message is displayed
cy.get('.welcome-message').should('contain', 'Welcome, TestUser');
});

it('should show error with incorrect credentials', () => {
// Type in the incorrect username and password
cy.get('#username').type('wronguser');
cy.get('#password').type('wrongpassword');

// Click the login button
cy.get('#loginButton').click();

// Verify that the error message is displayed
cy.get('.error-message').should('be.visible');
cy.get('.error-message').should('contain', 'Invalid credentials');
});
});

Let's break down this example:

  1. describe defines a test suite for the login flow
  2. beforeEach runs before each test, in this case visiting the login page
  3. it defines individual test cases
  4. cy.get() selects elements on the page using CSS selectors
  5. type() simulates typing text into inputs
  6. click() simulates clicking on elements
  7. should() makes assertions about what should be true

Understanding Cypress Commands

Cypress provides a rich set of commands for interacting with your application:

CommandDescription
cy.visit(url)Navigates to a specific URL
cy.get(selector)Gets DOM elements matching a selector
cy.contains(text)Gets elements containing specific text
cy.click()Clicks on an element
cy.type(text)Types text into an input field
cy.should(assertion)Makes an assertion about an element
cy.wait(time)Waits for a specified time in ms
cy.intercept(route, response)Intercepts network requests

Testing a Todo Application Example

Now let's look at a more complete example testing a todo application:

javascript
describe('Todo Application', () => {
beforeEach(() => {
// Visit the todo app and clear any existing todos
cy.visit('/');
cy.get('button.clear-all').click({ force: true });
});

it('should add a new todo item', () => {
// Type a new todo and press enter
cy.get('.new-todo')
.type('Learn Cypress{enter}');

// Verify the todo was added
cy.get('.todo-list li')
.should('have.length', 1)
.and('contain', 'Learn Cypress');
});

it('should mark a todo as completed', () => {
// Add a todo
cy.get('.new-todo')
.type('Learn E2E Testing{enter}');

// Click the checkbox to mark it as completed
cy.get('.todo-list li:first-child .toggle').click();

// Verify the todo is marked as completed
cy.get('.todo-list li:first-child')
.should('have.class', 'completed');
});

it('should delete a todo', () => {
// Add a todo
cy.get('.new-todo')
.type('Delete me{enter}');

// Hover over the todo to show the delete button
cy.get('.todo-list li:first-child')
.trigger('mouseover');

// Click the delete button
cy.get('.todo-list li:first-child .destroy')
.click({ force: true });

// Verify the todo list is empty
cy.get('.todo-list li')
.should('have.length', 0);
});
});

Testing API Interactions

E2E tests often need to deal with API calls. Cypress allows you to intercept network requests and provide mock responses:

javascript
describe('API Interactions', () => {
it('should display products from the API', () => {
// Intercept the API call and respond with mock data
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: 'Product 1', price: 9.99 },
{ id: 2, name: 'Product 2', price: 19.99 },
{ id: 3, name: 'Product 3', price: 29.99 }
]
}).as('getProducts');

// Visit the products page
cy.visit('/products');

// Wait for the API call to complete
cy.wait('@getProducts');

// Verify products are displayed
cy.get('.product-list .product')
.should('have.length', 3);

cy.get('.product-list .product:first-child')
.should('contain', 'Product 1')
.and('contain', '$9.99');
});

it('should show error when API fails', () => {
// Intercept the API call and respond with an error
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Server error' }
}).as('getProductsError');

// Visit the products page
cy.visit('/products');

// Wait for the API call to complete
cy.wait('@getProductsError');

// Verify error message is displayed
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Failed to load products');
});
});

Best Practices for E2E Testing

  1. Keep tests independent: Each test should be able to run on its own.
  2. Use meaningful selectors: Avoid using brittle selectors like CSS classes that might change.
  3. Test critical user flows: Focus on testing the important paths through your application.
  4. Use realistic data: Tests should use data that's representative of real user data.
  5. Keep tests fast: Slow tests can become a burden to maintain and run.
  6. Clean up after tests: Reset the application state between tests.
  7. Don't overuse waiting: Use explicit assertions rather than arbitrary wait times.

Handling Authentication in E2E Tests

Authentication is a common challenge in E2E testing. Here's how to handle it:

javascript
describe('Protected Routes', () => {
beforeEach(() => {
// Programmatically log in before each test
cy.login('[email protected]', 'password123');

// Verify we have a session cookie
cy.getCookie('session').should('exist');
});

it('should access a protected page', () => {
// Visit a protected page
cy.visit('/account');

// Verify we're on the account page, not redirected to login
cy.url().should('include', '/account');
cy.get('h1').should('contain', 'Account Settings');
});
});

// Define a custom command for login
// This should go in your cypress/support/commands.js file
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('#email').type(email);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('not.include', '/login');
});
});

CI/CD Integration

E2E tests are most valuable when integrated into your CI/CD pipeline. Here's a basic configuration for GitHub Actions:

yaml
name: E2E Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Cypress run
uses: cypress-io/github-action@v5
with:
build: npm run build
start: npm start
wait-on: 'http://localhost:3000'

Cross-Browser Testing

To ensure your application works across different browsers:

javascript
// cypress.config.js
module.exports = {
e2e: {
setupNodeEvents(on, config) {
return config;
},
baseUrl: 'http://localhost:3000',
browsers: ['chrome', 'firefox', 'edge'],
},
};

Summary

End-to-end testing is a crucial part of a comprehensive testing strategy for JavaScript applications. It helps ensure that your application functions correctly from the user's perspective, catching issues that might be missed by unit or integration tests.

In this guide, we've learned:

  • What E2E testing is and why it's important
  • How to set up Cypress for E2E testing
  • How to write basic E2E tests for user interactions
  • How to test API interactions and handle authentication
  • Best practices for writing maintainable E2E tests
  • How to integrate E2E tests into a CI/CD pipeline

By implementing E2E tests in your projects, you can have greater confidence in your application's functionality and catch regressions before they reach your users.

Additional Resources

Exercises

  1. Set up Cypress in an existing project and write a test that verifies your homepage loads correctly.
  2. Create a test for a form submission flow, including validation errors.
  3. Write a test that intercepts an API call and tests both success and error scenarios.
  4. Add authentication to your tests using custom commands.
  5. Configure your tests to run in a CI/CD pipeline using GitHub Actions or another CI system.

By practicing these exercises, you'll gain hands-on experience with E2E testing and improve the quality of your JavaScript applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)