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
)
-
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
, createCorsConfig.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("*"); } }
-
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
.
- Start the Spring Boot application by running the main class (
Step 2: Set Up a React Frontend
We’ll create a simple React app to interact with the Spring Boot API.
-
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
- In a new directory (e.g.,
-
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;
-
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; }
-
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.
- Start the React app:
Step 3: Write E2E Tests with Cypress
Cypress is a popular E2E testing framework that allows us to simulate user interactions in the browser.
-
Install Cypress: In the
spring-boot-frontend
directory, install Cypress:npm install cypress --save-dev
-
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 incypress/e2e
.
- Add a script to
-
Write E2E Tests: In
cypress/e2e
, create a file namedbook_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" ); }); });
-
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.
- Ensure both the Spring Boot backend (
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
-
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;
-
Install CORS: Install the
cors
package:npm install cors
-
Run the Express App:
- Start the Express app:
npm start
- The API should be available at
http://localhost:3000/api/users
.
- Start the Express app:
Step 2: Set Up a React Frontend
We’ll create a new React app to interact with the Express.js API.
-
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
- In a new directory (e.g.,
-
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;
-
Add Some Basic Styling: Replace
src/App.css
with the same CSS as in the Spring Boot frontend (see above). -
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.
- Start the React app:
Step 3: Write E2E Tests with Cypress
We’ll use Cypress again to write E2E tests for the Express.js frontend.
-
Install Cypress: In the
express-frontend
directory, install Cypress:npm install cypress --save-dev
-
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
- Add a script to
-
Write E2E Tests: In
cypress/e2e
, create a file nameduser_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])" ); }); });
-
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). Updateserver.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 theexpress-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
- 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.
- Full Application Flow: We tested the entire stack—frontend, backend, and database—ensuring that all components work together as expected.
- Cypress Features: Cypress provides a powerful API for interacting with the DOM (
cy.get()
,cy.click()
), making assertions (should()
), and handling asynchronous behavior. - 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