Lab 2 - Integration Testing Tutorial: Spring Boot and Express.js
In this tutorial, I’ll guide you through integration testing for both Spring Boot (Java) and Express.js (Node.js) applications. Unlike unit tests, which focus on isolated components, integration tests verify that different parts of the application (e.g., controllers, services, and databases) work together correctly. 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 include a real database for testing.
We’ll use:
- Spring Boot: H2 in-memory database with
@SpringBootTest
for integration testing. - Express.js: SQLite with
better-sqlite3
for a lightweight database, and Mocha/Chai/Supertest for testing.
Overview
Integration testing ensures that the components of your application work together as expected. For a REST API, this typically means testing the full stack (controller, service, and database) by making real HTTP requests and interacting with a database. We’ll set up an in-memory database for both applications to keep the tests fast and independent of external systems.
Part 1: Spring Boot Integration Testing
Step 1: Set Up a Spring Boot Project with a Database
We’ll extend the book management API from the previous tutorial by adding a database layer using Spring Data JPA and an H2 in-memory database for testing.
-
Add Dependencies: Open your Spring Boot project’s
pom.xml
and ensure the following dependencies are included (they might already be there if you followed the previous tutorial):<dependencies> <!-- Spring Web for REST API --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Data JPA for database access --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- H2 Database for in-memory testing --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
Configure H2 Database: In
src/main/resources/application.properties
, add the following to configure the H2 in-memory database:spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true spring.jpa.hibernate.ddl-auto=update
-
Update the Book Model for JPA: Modify
Book.java
insrc/main/java/com/example/demo/model
to make it a JPA entity:package com.example.demo.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String author; // Default constructor for JPA public Book() {} public Book(Long id, String title, String author) { this.id = id; this.title = title; this.author = author; } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } }
-
Create a Book Repository: In
src/main/java/com/example/demo/repository
, createBookRepository.java
:package com.example.demo.repository; import com.example.demo.model.Book; import org.springframework.data.jpa.repository.JpaRepository; public interface BookRepository extends JpaRepository<Book, Long> { }
-
Update the Book Service: Modify
BookService.java
insrc/main/java/com/example/demo/service
to use the repository:package com.example.demo.service; import com.example.demo.model.Book; import com.example.demo.repository.BookRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class BookService { @Autowired private BookRepository bookRepository; public List<Book> getAllBooks() { return bookRepository.findAll(); } public Book getBookById(Long id) { Optional<Book> book = bookRepository.findById(id); return book.orElse(null); } public Book saveBook(Book book) { return bookRepository.save(book); } }
-
Update the Book Controller: Add a POST endpoint to
BookController.java
insrc/main/java/com/example/demo/controller
:package com.example.demo.controller; import com.example.demo.model.Book; import com.example.demo.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/books") public class BookController { @Autowired private BookService bookService; @GetMapping public List<Book> getAllBooks() { return bookService.getAllBooks(); } @GetMapping("/{id}") public ResponseEntity<Book> getBookById(@PathVariable Long id) { Book book = bookService.getBookById(id); if (book == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(book); } @PostMapping public Book createBook(@RequestBody Book book) { return bookService.saveBook(book); } }
Step 2: Write Integration Tests for Spring Boot
We’ll use @SpringBootTest
to load the full application context and @AutoConfigureMockMvc
to test HTTP requests. The H2 database will be used automatically.
-
Create an Integration Test Class: In
src/test/java/com/example/demo
, createBookControllerIntegrationTest.java
:package com.example.demo; import com.example.demo.model.Book; import com.example.demo.repository.BookRepository; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc public class BookControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private BookRepository bookRepository; @Autowired private ObjectMapper objectMapper; @BeforeEach public void setUp() { bookRepository.deleteAll(); bookRepository.save(new Book(null, "The Great Gatsby", "F. Scott Fitzgerald")); bookRepository.save(new Book(null, "1984", "George Orwell")); } @Test public void testGetAllBooks() throws Exception { mockMvc.perform(get("/api/books") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].title", is("The Great Gatsby"))) .andExpect(jsonPath("$[1].title", is("1984"))); } @Test public void testGetBookById() throws Exception { Book book = bookRepository.findAll().get(0); mockMvc.perform(get("/api/books/" + book.getId()) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.title", is(book.getTitle()))); } @Test public void testCreateBook() throws Exception { Book newBook = new Book(null, "To Kill a Mockingbird", "Harper Lee"); mockMvc.perform(post("/api/books") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(newBook))) .andExpect(status().isOk()) .andExpect(jsonPath("$.title", is("To Kill a Mockingbird"))) .andExpect(jsonPath("$.author", is("Harper Lee"))); } }
-
Run the Tests:
- Right-click on
BookControllerIntegrationTest.java
in your IDE and select “Run Tests.” - The tests should pass, confirming that the controller, service, and database work together correctly.
- Right-click on
Spring Boot Lab 1: Add a New Integration Test
- Task: Write an integration test for a new endpoint in
BookController
that updates a book (PUT/api/books/{id}
). The endpoint should update the book’s title and author and return the updated book. - Hint: Use
put()
inMockMvc
to send the request, and verify the updated book in the database usingbookRepository
.
Part 2: Express.js Integration Testing
Step 1: Set Up an Express.js Project with a Database
We’ll extend the user management API from the previous tutorial by adding SQLite as the database using better-sqlite3
.
-
Install Dependencies: In your
express-user-api
project, install the SQLite dependency:npm install better-sqlite3
-
Set Up SQLite Database: Create a
database.js
file in the project root to initialize the SQLite database:const Database = require("better-sqlite3"); const db = new Database(":memory:"); // In-memory database for testing db.exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL ) `); module.exports = db;
-
Update the Express App: Modify
app.js
to use the SQLite database:const express = require("express"); const db = require("./database"); const app = express(); 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;
Step 2: Write Integration Tests for Express.js
We’ll use Mocha, Chai, and Supertest to test the API, interacting with the SQLite database.
-
Update the Test File: In
test/app.test.js
, modify the tests to include database setup and integration testing:const request = require("supertest"); const app = require("../app"); const expect = require("chai").expect; const db = require("../database"); describe("User API Integration Tests", () => { beforeEach(() => { // Clear the database and seed it with initial data db.exec("DELETE FROM users"); const insert = db.prepare( "INSERT INTO users (name, email) VALUES (?, ?)" ); insert.run("Alice", "[email protected]"); insert.run("Bob", "[email protected]"); }); describe("GET /api/users", () => { it("should return a list of users", async () => { const res = await request(app) .get("/api/users") .expect("Content-Type", /json/) .expect(200); expect(res.body).to.be.an("array"); expect(res.body).to.have.lengthOf(2); expect(res.body[0]).to.have.property("name", "Alice"); }); }); describe("GET /api/users/:id", () => { it("should return a user by ID", async () => { const user = db .prepare("SELECT * FROM users WHERE name = ?") .get("Alice"); const res = await request(app) .get(`/api/users/${user.id}`) .expect("Content-Type", /json/) .expect(200); expect(res.body).to.have.property("id", user.id); expect(res.body).to.have.property("name", "Alice"); }); it("should return 404 if user not found", async () => { await request(app) .get("/api/users/999") .expect("Content-Type", /json/) .expect(404); }); }); describe("POST /api/users", () => { it("should create a new user", async () => { const newUser = { name: "Charlie", email: "[email protected]" }; const res = await request(app) .post("/api/users") .send(newUser) .expect("Content-Type", /json/) .expect(201); expect(res.body).to.have.property("name", "Charlie"); expect(res.body).to.have.property("email", "[email protected]"); const dbUser = db .prepare("SELECT * FROM users WHERE name = ?") .get("Charlie"); expect(dbUser).to.exist; expect(dbUser.email).to.equal("[email protected]"); }); }); });
-
Run the Tests:
- In the terminal, run:
npm test
- The tests should pass, confirming that the API interacts correctly with the SQLite database.
- In the terminal, run:
Express.js Lab 2: Add a New Integration Test
- Task: Write an integration test for a new endpoint in
app.js
that updates a user (PUT/api/users/:id
). The endpoint should update the user’s name and email and return the updated user. - Hint: Use
request(app).put('/api/users/:id')
with.send({ name: 'Updated Name', email: '[email protected]' })
to send the payload. Verify the updated user in the database using a query.
Key Concepts and Best Practices
- Full Stack Testing: In both Spring Boot and Express.js, we tested the entire stack (controller/routes, service, and database) by making real HTTP requests and interacting with a database.
- In-Memory Databases: We used H2 for Spring Boot and SQLite in-memory for Express.js to keep tests fast and independent of external systems.
- Setup and Teardown: The
@BeforeEach
in Spring Boot andbeforeEach
in Mocha ensured a clean database state for each test, avoiding test interference. - Realistic Scenarios: Integration tests simulate real-world usage, including database operations, making them more comprehensive than unit tests.
Additional Notes
- Spring Boot: You can use
@DataJpaTest
for repository-layer tests if you want to focus on database interactions without loading the full application context.@SpringBootTest
is better for full integration tests. - Express.js: For production applications, you might use a different database (e.g., PostgreSQL or MongoDB). In that case, consider using a test database or Docker to manage test environments.
- Performance: Integration tests are slower than unit tests because they involve real database operations. Use them sparingly and complement them with unit tests for faster feedback.
This tutorial provides a solid foundation for integration testing in Spring Boot and Express.js. By completing the labs, you’ll gain hands-on experience in setting up and testing full-stack applications, a critical skill for building robust software.
By Wahid Hamdi