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:

  1. A Spring Boot API to manage a list of books.
  2. 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.

  1. 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).
  2. Create a Book Model: In src/main/java/com/example/demo/model, create a Book.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; }
    }
  3. Create a Book Service: In src/main/java/com/example/demo/service, create a BookService.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);
        }
    }
  4. Create a Book Controller: In src/main/java/com/example/demo/controller, create a BookController.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.

  1. Create a Test Class for the Controller: In src/test/java/com/example/demo/controller, create BookControllerTest.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());
        }
    }
  2. 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.

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. Use content() to set the request body and contentType(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.

  1. 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
  2. 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;
  3. 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}`);
    });
  4. Update package.json for Testing: Add a test script to package.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.

  1. Create a Test Directory and File:

    • Create a test directory in the project root.
    • Inside test, create app.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);
        });
      });
    });
  2. Run the Tests:

    • In the terminal, run:
      npm test
    • You should see all tests pass, confirming the API endpoints work as expected.

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 with name and email, assign a new id, 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

  1. 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).
  2. 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).
  3. Assertions: Spring Boot tests used Hamcrest matchers (jsonPath), while Express.js tests used Chai’s expect for readable assertions.
  4. 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