Lab 3 - E2E Testing Tutorial: Spring Boot and Express.js


In this tutorial, I’ll guide you through end-to-end (E2E) testing for both Spring Boot (Java) and Express.js (Node.js) applications. E2E testing verifies the entire application flow from the user’s perspective, simulating real user interactions with the system, including the frontend, backend, and database. We’ll build on the previous examples of a book management API in Spring Boot and a user management API in Express.js, but this time we’ll add a simple frontend and use E2E testing tools to interact with the application as a user would.

We’ll use:

  • Spring Boot: A simple React frontend with Cypress for E2E testing.
  • Express.js: A simple React frontend with Cypress for E2E testing.
  • Database: H2 in-memory database for Spring Boot and SQLite for Express.js, as in the integration testing tutorial.

Overview

E2E testing ensures that the entire application—frontend, backend, and database—works together as expected from the user’s perspective. We’ll create a minimal React frontend for both applications, deploy the backend APIs, and use Cypress to simulate user interactions like clicking buttons, filling forms, and verifying UI updates.


Part 1: Spring Boot E2E Testing

Step 1: Set Up the Spring Boot Backend

We’ll use the same book management API from the integration testing tutorial, which already includes a database layer with H2 and Spring Data JPA. Ensure your Spring Boot project is set up as described in the integration testing tutorial, with the following components:

  • Book model (JPA entity)
  • BookRepository (JPA repository)
  • BookService (service layer)
  • BookController (REST controller with GET /api/books, GET /api/books/{id}, and POST /api/books)
  1. Enable CORS: Since we’ll be accessing the Spring Boot API from a React frontend, we need to enable CORS. Add a CORS configuration to your Spring Boot application. In src/main/java/com/example/demo, create CorsConfig.java:

    package com.example.demo;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("http://localhost:3000") // React app URL
                    .allowedMethods("GET", "POST", "PUT", "DELETE")
                    .allowedHeaders("*");
        }
    }
  2. Run the Spring Boot Application:

    • Start the Spring Boot application by running the main class (DemoApplication.java) in your IDE or using:
      ./mvnw spring-boot:run
    • The API should be available at http://localhost:8080/api/books.

Step 2: Set Up a React Frontend

We’ll create a simple React app to interact with the Spring Boot API.

  1. Create a React App:

    • In a new directory (e.g., spring-boot-frontend), create a React app:
      npx create-react-app spring-boot-frontend
      cd spring-boot-frontend
    • Install axios to make HTTP requests to the Spring Boot API:
      npm install axios
  2. Create a Component to Display and Add Books: Replace src/App.js with the following:

    import React, { useState, useEffect } from "react";
    import axios from "axios";
    import "./App.css";
    
    function App() {
      const [books, setBooks] = useState([]);
      const [title, setTitle] = useState("");
      const [author, setAuthor] = useState("");
    
      useEffect(() => {
        fetchBooks();
      }, []);
    
      const fetchBooks = async () => {
        const response = await axios.get("http://localhost:8080/api/books");
        setBooks(response.data);
      };
    
      const addBook = async (e) => {
        e.preventDefault();
        await axios.post("http://localhost:8080/api/books", { title, author });
        setTitle("");
        setAuthor("");
        fetchBooks();
      };
    
      return (
        <div className="App">
          <h1>Book Management</h1>
          <form onSubmit={addBook}>
            <input
              type="text"
              placeholder="Title"
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              data-cy="title-input"
            />
            <input
              type="text"
              placeholder="Author"
              value={author}
              onChange={(e) => setAuthor(e.target.value)}
              data-cy="author-input"
            />
            <button type="submit" data-cy="add-book-button">
              Add Book
            </button>
          </form>
          <h2>Books</h2>
          <ul data-cy="book-list">
            {books.map((book) => (
              <li key={book.id}>
                {book.title} by {book.author}
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default App;
  3. Add Some Basic Styling: Replace src/App.css with:

    .App {
      text-align: center;
      padding: 20px;
    }
    
    form {
      margin-bottom: 20px;
    }
    
    input {
      margin: 5px;
      padding: 5px;
    }
    
    button {
      padding: 5px 10px;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      margin: 5px 0;
    }
  4. Run the React App:

    • Start the React app:
      npm start
    • The app should be available at http://localhost:3000. You should be able to see a form to add books and a list of books fetched from the Spring Boot API.

Step 3: Write E2E Tests with Cypress

Cypress is a popular E2E testing framework that allows us to simulate user interactions in the browser.

  1. Install Cypress: In the spring-boot-frontend directory, install Cypress:

    npm install cypress --save-dev
  2. Configure Cypress:

    • Add a script to package.json to run Cypress:
      "scripts": {
          "start": "react-scripts start",
          "cypress:open": "cypress open"
      }
    • Run Cypress to initialize it:
      npm run cypress:open
    • This will create a cypress directory with example files. You can delete the examples in cypress/e2e.
  3. Write E2E Tests: In cypress/e2e, create a file named book_management.cy.js:

    describe("Book Management E2E Tests", () => {
      beforeEach(() => {
        // Visit the app before each test
        cy.visit("http://localhost:3000");
      });
    
      it("should display the initial list of books", () => {
        cy.get("[data-cy=book-list]").children().should("have.length", 2);
        cy.get("[data-cy=book-list]").should("contain", "The Great Gatsby");
        cy.get("[data-cy=book-list]").should("contain", "1984");
      });
    
      it("should add a new book", () => {
        cy.get("[data-cy=title-input]").type("To Kill a Mockingbird");
        cy.get("[data-cy=author-input]").type("Harper Lee");
        cy.get("[data-cy=add-book-button]").click();
    
        cy.get("[data-cy=book-list]").children().should("have.length", 3);
        cy.get("[data-cy=book-list]").should(
          "contain",
          "To Kill a Mockingbird by Harper Lee"
        );
      });
    });
  4. Run the E2E Tests:

    • Ensure both the Spring Boot backend (http://localhost:8080) and the React frontend (http://localhost:3000) are running.
    • Run Cypress:
      npm run cypress:open
    • In the Cypress UI, click on book_management.cy.js to run the tests. You should see the tests pass, confirming that the app works as expected from a user’s perspective.

Spring Boot Lab 1: Add a New E2E Test

  • Task: Write an E2E test to verify that clicking on a book in the list displays its details (e.g., by adding a “View Details” button for each book that shows the book’s title and author in a modal or separate section).
  • Hint: Add a button to each book in the list in App.js, update the UI to show details, and write a Cypress test to click the button and verify the details.

Part 2: Express.js E2E Testing

Step 1: Set Up the Express.js Backend

We’ll use the same user management API from the integration testing tutorial, which uses SQLite as the database. Ensure your Express.js project (express-user-api) is set up as described, with the following endpoints:

  • GET /api/users
  • GET /api/users/:id
  • POST /api/users
  1. Enable CORS: Add CORS support to app.js so the React frontend can access the API:

    const express = require("express");
    const db = require("./database");
    const cors = require("cors");
    const app = express();
    
    app.use(cors({ origin: "http://localhost:3000" }));
    app.use(express.json());
    
    // Seed the database with initial data
    const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    insert.run("Alice", "[email protected]");
    insert.run("Bob", "[email protected]");
    
    app.get("/api/users", (req, res) => {
      const users = db.prepare("SELECT * FROM users").all();
      res.json(users);
    });
    
    app.get("/api/users/:id", (req, res) => {
      const user = db
        .prepare("SELECT * FROM users WHERE id = ?")
        .get(req.params.id);
      if (!user) {
        return res.status(404).json({ message: "User not found" });
      }
      res.json(user);
    });
    
    app.post("/api/users", (req, res) => {
      const { name, email } = req.body;
      const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
      const result = insert.run(name, email);
      const newUser = db
        .prepare("SELECT * FROM users WHERE id = ?")
        .get(result.lastInsertRowid);
      res.status(201).json(newUser);
    });
    
    module.exports = app;
  2. Install CORS: Install the cors package:

    npm install cors
  3. Run the Express App:

    • Start the Express app:
      npm start
    • The API should be available at http://localhost:3000/api/users.

Step 2: Set Up a React Frontend

We’ll create a new React app to interact with the Express.js API.

  1. Create a React App:

    • In a new directory (e.g., express-frontend), create a React app:
      npx create-react-app express-frontend
      cd express-frontend
    • Install axios:
      npm install axios
  2. Create a Component to Display and Add Users: Replace src/App.js with the following:

    import React, { useState, useEffect } from "react";
    import axios from "axios";
    import "./App.css";
    
    function App() {
      const [users, setUsers] = useState([]);
      const [name, setName] = useState("");
      const [email, setEmail] = useState("");
    
      useEffect(() => {
        fetchUsers();
      }, []);
    
      const fetchUsers = async () => {
        const response = await axios.get("http://localhost:3000/api/users");
        setUsers(response.data);
      };
    
      const addUser = async (e) => {
        e.preventDefault();
        await axios.post("http://localhost:3000/api/users", { name, email });
        setName("");
        setEmail("");
        fetchUsers();
      };
    
      return (
        <div className="App">
          <h1>User Management</h1>
          <form onSubmit={addUser}>
            <input
              type="text"
              placeholder="Name"
              value={name}
              onChange={(e) => setName(e.target.value)}
              data-cy="name-input"
            />
            <input
              type="text"
              placeholder="Email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              data-cy="email-input"
            />
            <button type="submit" data-cy="add-user-button">
              Add User
            </button>
          </form>
          <h2>Users</h2>
          <ul data-cy="user-list">
            {users.map((user) => (
              <li key={user.id}>
                {user.name} ({user.email})
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default App;
  3. Add Some Basic Styling: Replace src/App.css with the same CSS as in the Spring Boot frontend (see above).

  4. Run the React App:

    • Start the React app:
      npm start
    • The app should be available at http://localhost:3000. You should be able to see a form to add users and a list of users fetched from the Express.js API.

Step 3: Write E2E Tests with Cypress

We’ll use Cypress again to write E2E tests for the Express.js frontend.

  1. Install Cypress: In the express-frontend directory, install Cypress:

    npm install cypress --save-dev
  2. Configure Cypress:

    • Add a script to package.json:
      "scripts": {
          "start": "react-scripts start",
          "cypress:open": "cypress open"
      }
    • Run Cypress to initialize it:
      npm run cypress:open
  3. Write E2E Tests: In cypress/e2e, create a file named user_management.cy.js:

    describe("User Management E2E Tests", () => {
      beforeEach(() => {
        cy.visit("http://localhost:3000");
      });
    
      it("should display the initial list of users", () => {
        cy.get("[data-cy=user-list]").children().should("have.length", 2);
        cy.get("[data-cy=user-list]").should("contain", "Alice");
        cy.get("[data-cy=user-list]").should("contain", "Bob");
      });
    
      it("should add a new user", () => {
        cy.get("[data-cy=name-input]").type("Charlie");
        cy.get("[data-cy=email-input]").type("[email protected]");
        cy.get("[data-cy=add-user-button]").click();
    
        cy.get("[data-cy=user-list]").children().should("have.length", 3);
        cy.get("[data-cy=user-list]").should(
          "contain",
          "Charlie ([email protected])"
        );
      });
    });
  4. Run the E2E Tests:

    • Ensure both the Express.js backend (http://localhost:3000) and the React frontend (http://localhost:3000) are running. Since both want to use port 3000, you’ll need to run the Express app on a different port (e.g., 3001). Update server.js to use port 3001:

      const app = require("./app");
      const port = 3001;
      
      app.listen(port, () => {
        console.log(`Server running on port ${port}`);
      });
    • Update App.js in the express-frontend to point to the new port:

      const response = await axios.get("http://localhost:3001/api/users");
      // And for the POST request:
      await axios.post("http://localhost:3001/api/users", { name, email });
    • Update the CORS configuration in app.js to allow the new port:

      app.use(cors({ origin: "http://localhost:3000" }));
    • Start the Express app:

      npm start
    • Start the React app:

      npm start
    • Run Cypress:

      npm run cypress:open
    • In the Cypress UI, click on user_management.cy.js to run the tests. The tests should pass.

Express.js Lab 2: Add a New E2E Test

  • Task: Write an E2E test to verify that clicking on a user in the list displays their details (e.g., by adding a “View Details” button for each user that shows the user’s name and email in a modal or separate section).
  • Hint: Add a button to each user in the list in App.js, update the UI to show details, and write a Cypress test to click the button and verify the details.

Key Concepts and Best Practices

  1. User Perspective: E2E tests simulate real user interactions, such as clicking buttons, filling forms, and verifying UI updates, making them the most comprehensive type of testing.
  2. Full Application Flow: We tested the entire stack—frontend, backend, and database—ensuring that all components work together as expected.
  3. Cypress Features: Cypress provides a powerful API for interacting with the DOM (cy.get(), cy.click()), making assertions (should()), and handling asynchronous behavior.
  4. Test Isolation: The beforeEach hook ensures each test starts with a fresh state by visiting the app’s homepage.

Additional Notes

  • Spring Boot: If your application requires authentication, you can use Cypress to log in before running tests (e.g., by filling out a login form or setting a token in local storage).
  • Express.js: For more complex applications, you might need to mock external APIs or services during E2E testing. Cypress supports network stubbing with cy.intercept().
  • Performance: E2E tests are the slowest type of test because they involve the entire application stack and a real browser. Use them for critical user flows and complement them with unit and integration tests for faster feedback.
  • CI/CD Integration: Both Spring Boot and Express.js E2E tests can be run in a CI/CD pipeline using Cypress’s cypress run command for headless testing.

This tutorial provides a foundation for E2E testing in Spring Boot and Express.js with a React frontend. By completing the labs, you’ll gain hands-on experience in testing full application flows, a crucial skill for ensuring a seamless user experience.


By Wahid Hamdi