Lab 1 - Unit Testing Tutorial: Spring Boot and Express.js
Unit testing is a critical skill for ensuring code reliability, and we’ll focus on testing a simple REST API in both frameworks. We’ll use JUnit and Mockito for Spring Boot, and Mocha, Chai, and Supertest for Express.js. The tutorial will include practical labs to help you apply these concepts.
Overview
Unit testing involves testing individual components (or “units”) of your application in isolation. For a REST API, this typically means testing controllers, services, and routes without involving external dependencies like databases or network calls. We’ll mock these dependencies to keep tests fast and independent.
We’ll build two simple REST APIs:
- A Spring Boot API to manage a list of books.
- An Express.js API to manage a list of users.
Then, we’ll write unit tests for both applications.
Part 1: Spring Boot Unit Testing
Step 1: Set Up a Spring Boot Project
Let’s create a simple Spring Boot application to manage books.
-
Create a Spring Boot Project:
- Use Spring Initializr to generate a project with the following dependencies:
- Spring Web
- Spring Boot Starter Test (for testing)
- Download, unzip, and open the project in your IDE (e.g., IntelliJ IDEA or VS Code).
- Use Spring Initializr to generate a project with the following dependencies:
-
Create a Book Model: In
src/main/java/com/example/demo/model
, create aBook.java
class:package com.example.demo.model; public class Book { private Long id; private String title; private String author; // Constructor 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 Service: In
src/main/java/com/example/demo/service
, create aBookService.java
class:package com.example.demo.service; import com.example.demo.model.Book; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; @Service public class BookService { public List<Book> getAllBooks() { return Arrays.asList( new Book(1L, "The Great Gatsby", "F. Scott Fitzgerald"), new Book(2L, "1984", "George Orwell") ); } public Book getBookById(Long id) { return getAllBooks().stream() .filter(book -> book.getId().equals(id)) .findFirst() .orElse(null); } }
-
Create a Book Controller: In
src/main/java/com/example/demo/controller
, create aBookController.java
class: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.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; 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); } }
Step 2: Write Unit Tests for Spring Boot
Spring Boot’s spring-boot-starter-test
dependency includes JUnit, Mockito, and other testing libraries. We’ll use @WebMvcTest
to test the controller in isolation by mocking the service layer.
-
Create a Test Class for the Controller: In
src/test/java/com/example/demo/controller
, createBookControllerTest.java
:package com.example.demo.controller; import com.example.demo.model.Book; import com.example.demo.service.BookService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.util.Arrays; import java.util.List; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(BookController.class) public class BookControllerTest { @Autowired private MockMvc mockMvc; @MockBean private BookService bookService; @Test public void testGetAllBooks() throws Exception { List<Book> books = Arrays.asList( new Book(1L, "The Great Gatsby", "F. Scott Fitzgerald"), new Book(2L, "1984", "George Orwell") ); when(bookService.getAllBooks()).thenReturn(books); mockMvc.perform(MockMvcRequestBuilders.get("/api/books") .accept(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_Found() throws Exception { Book book = new Book(1L, "The Great Gatsby", "F. Scott Fitzgerald"); when(bookService.getBookById(1L)).thenReturn(book); mockMvc.perform(MockMvcRequestBuilders.get("/api/books/1") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.title", is("The Great Gatsby"))) .andExpect(jsonPath("$.author", is("F. Scott Fitzgerald"))); } @Test public void testGetBookById_NotFound() throws Exception { when(bookService.getBookById(3L)).thenReturn(null); mockMvc.perform(MockMvcRequestBuilders.get("/api/books/3") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); } }
-
Run the Tests:
- Right-click on
BookControllerTest.java
in your IDE and select “Run Tests.” - All tests should pass, confirming that the controller behaves as expected.
- Right-click on
Spring Boot Lab 1: Add a New Test Case
- Task: Write a unit test for a new endpoint in
BookController
that adds a book (POST/api/books
). Mock the service layer to return the newly added book. - Hint: Use
MockMvcRequestBuilders.post()
and pass a JSON payload for the new book. Usecontent()
to set the request body andcontentType(MediaType.APPLICATION_JSON)
to set the content type.
Part 2: Express.js Unit Testing
Step 1: Set Up an Express.js Project
Let’s create a simple Express.js application to manage users.
-
Initialize a Node.js Project:
- Create a new directory called
express-user-api
and navigate to it:mkdir express-user-api cd express-user-api npm init -y
- Install Express and testing dependencies:
npm install express npm install --save-dev mocha chai supertest sinon
- Create a new directory called
-
Create the Express App: In the project root, create
app.js
:const express = require("express"); const app = express(); app.use(express.json()); const users = [ { id: 1, name: "Alice", email: "[email protected]" }, { id: 2, name: "Bob", email: "[email protected]" }, ]; app.get("/api/users", (req, res) => { res.json(users); }); app.get("/api/users/:id", (req, res) => { const user = users.find((u) => u.id === parseInt(req.params.id)); if (!user) { return res.status(404).json({ message: "User not found" }); } res.json(user); }); module.exports = app;
-
Create a Server File: In the project root, create
server.js
:const app = require("./app"); const port = 3000; app.listen(port, () => { console.log(`Server running on port ${port}`); });
-
Update
package.json
for Testing: Add a test script topackage.json
:"scripts": { "start": "node server.js", "test": "mocha" }
Step 2: Write Unit Tests for Express.js
We’ll use Mocha as the test runner, Chai for assertions, and Supertest to simulate HTTP requests.
-
Create a Test Directory and File:
- Create a
test
directory in the project root. - Inside
test
, createapp.test.js
:
const request = require("supertest"); const app = require("../app"); const expect = require("chai").expect; describe("User API", () => { 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 res = await request(app) .get("/api/users/1") .expect("Content-Type", /json/) .expect(200); expect(res.body).to.have.property("id", 1); 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); }); }); });
- Create a
-
Run the Tests:
- In the terminal, run:
npm test
- You should see all tests pass, confirming the API endpoints work as expected.
- In the terminal, run:
Express.js Lab 2: Add a New Test Case
- Task: Write a unit test for a new endpoint in
app.js
that adds a user (POST/api/users
). The endpoint should accept a JSON payload withname
andemail
, assign a newid
, and return the new user. - Hint: Use
request(app).post('/api/users')
with.send({ name: 'Charlie', email: '[email protected]' })
to send the payload. Assert the response status and body.
Key Concepts and Best Practices
- Isolation: In both Spring Boot and Express.js tests, we isolated the unit under test (controller/routes) by mocking dependencies (e.g.,
BookService
in Spring Boot, or avoiding real database calls in Express.js). - Mocking: We used Mockito in Spring Boot (
@MockBean
) and could use Sinon in Express.js for more complex mocking scenarios (e.g., mocking a database service). - Assertions: Spring Boot tests used Hamcrest matchers (
jsonPath
), while Express.js tests used Chai’sexpect
for readable assertions. - Test Structure: Both frameworks benefit from a clear test structure (e.g.,
describe
blocks in Mocha, or JUnit’s@Test
methods).
Additional Notes
- Spring Boot: If you want to test the service layer or integrate with a database, you can use
@DataJpaTest
or@SpringBootTest
for integration tests. However, for unit tests,@WebMvcTest
is ideal for controllers. - Express.js: For more advanced testing, consider using Sinon to mock external services or rewire to mock private functions. Integration tests can involve a real database, but unit tests should avoid such dependencies.
This tutorial provides a foundation for unit testing in both Spring Boot and Express.js. By completing the labs, you’ll gain hands-on experience in writing and running tests, a crucial skill for any developer.
By Wahid Hamdi