Skip to content
ganmath edited this page Apr 26, 2024 · 5 revisions

Detailed lab for implementing a Domain-Driven Design (DDD) project using Spring Boot. This lab focuses on creating a simple Book Management System with CRUD operations, using PostgreSQL as the database. Each step includes implementing the functionality and writing JUnit tests.

Prerequisites

  • Java Development Kit (JDK) 11 or later
  • PostgreSQL installed and running
  • An IDE that supports Spring, such as IntelliJ IDEA or Eclipse
  • Maven for dependency management and project building

Step 1: Set Up Spring Boot Project

  1. Create a new Spring Boot project using Spring Initializr:

    • Choose Maven Project with Java.
    • Add dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Spring Boot Test.
    • Download and open it in your IDE.
  2. Configure application properties in src/main/resources/application.properties:

    spring.datasource.url=jdbc:postgresql://localhost:5432/bookdb
    spring.datasource.username=postgres
    spring.datasource.password=your_password
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true

Step 2: Domain Model

  1. Create the Book entity in src/main/java/com/example/library/domain/model/Book.java:
    package com.example.library.domain.model;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    @Entity
    public class Book {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String title;
        private String author;
        private String isbn;
    
        // Constructors, getters, and setters
    }

Step 3: Repository

  1. Create the BookRepository interface in src/main/java/com/example/library/domain/repository/BookRepository.java:
    package com.example.library.domain.repository;
    
    import com.example.library.domain.model.Book;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface BookRepository extends JpaRepository<Book, Long> {
    }

Step 4: Service Layer

  1. Create the BookService class in src/main/java/com/example/library/domain/service/BookService.java:
    package com.example.library.domain.service;
    
    import com.example.library.domain.model.Book;
    import com.example.library.domain.repository.BookRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service
    public class BookService {
        private final BookRepository bookRepository;
    
        @Autowired
        public BookService(BookRepository bookRepository) {
            this.bookRepository = bookRepository;
        }
    
        public Book saveBook(Book book) {
            return bookRepository.save(book);
        }
    
        public List<Book> findAllBooks() {
            return bookRepository.findAll();
        }
    
        public Book findBookById(Long id) {
            return bookRepository.findById(id).orElse(null);
        }
    
        public void deleteBook(Long id) {
            bookRepository.deleteById(id);
        }
    }

Step 5: Controller

  1. Create the BookController class in src/main/java/com/example/library/application/BookController.java:
    package com.example.library.application;
    
    import com.example.library.domain.model.Book;
    import com.example.library.domain.service.BookService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/books")
    public class BookController {
        private final BookService bookService;
    
        @Autowired
        public BookController(BookService bookService) {
            this.bookService = bookService;
        }
    
        @PostMapping
        public Book addBook(@RequestBody Book book) {
            return bookService.saveBook(book);
        }
    
        @GetMapping
        public List<Book> getAllBooks() {
            return bookService.findAllBooks();
        }
    
        @GetMapping("/{id}")
        public Book getBookById(@PathVariable Long id) {
            return bookService.findBookById(id);
        }
    
        @DeleteMapping("/{id}")
        public void deleteBook(@PathVariable Long id) {
            bookService.deleteBook(id);
        }
    }

Step 6: JUnit Tests

  1. Write unit tests in src/test/java/com/example/library/domain/service/BookServiceTests.java:
    package com.example.library.domain.service;
    
    import com.example.library.domain.model.Book;
    import com.example.library.domain.repository.BookRepository;
    import org.junit.jupiter.api.Test;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.junit.jupiter.MockitoExtension;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.util.Optional;
    
    import static org.mockito.Mockito.*;
    import static org.junit.jupiter.api.Assertions.*;
    
    @SpringBootTest
    @ExtendWith(MockitoExtension.class)
    public class BookServiceTests {
        @Mock
        private BookRepository bookRepository;
        @InjectMocks
        private BookService bookService;
    
        @Test
        public void testSaveBook() {
            Book book = new Book("1984", "George Orwell", "1234567890");
            when(bookRepository.save(any(Book.class))).thenReturn(book);
            Book savedBook = bookService.saveBook(book);
            assertNotNull(savedBook);
            assertEquals("1984", savedBook.getTitle());
        }
    
        @Test
        public void testFindBookById() {
            Book book = new Book("1984", "George Orwell", "1234567890");
            when(bookRepository.findById(anyLong())).thenReturn(Optional.of(book));
            Book foundBook = bookService.findBookById(1L);
            assertNotNull(foundBook);
            assertEquals("1984", foundBook.getTitle());
        }
    }

Step 1: Testing the Repository Layer

We'll start with more tests for the BookRepository, although usually, it might not need extensive testing if it extends Spring Data JPA's interfaces directly. However, for custom queries, it's good to test those.

package com.example.library.domain.repository;

import com.example.library.domain.model.Book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
public class BookRepositoryTests {

    @Autowired
    private BookRepository bookRepository;

    @Test
    void testSaveBook() {
        Book book = new Book("Sapiens", "Yuval Noah Harari", "987654321");
        book = bookRepository.save(book);
        assertThat(book.getId()).isNotNull();
    }

    @Test
    void testDeleteBook() {
        Book book = new Book("Sapiens", "Yuval Noah Harari", "987654321");
        book = bookRepository.save(book);
        bookRepository.delete(book);
        assertThat(bookRepository.findById(book.getId())).isEmpty();
    }
}

Step 2: Expanded Testing for the Service Layer

These tests use Mockito to mock the repository interactions and focus on the business logic in the service layer.

package com.example.library.domain.service;

import com.example.library.domain.model.Book;
import com.example.library.domain.repository.BookRepository;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class BookServiceTests {

    @Mock
    private BookRepository bookRepository;

    @InjectMocks
    private BookService bookService;

    @Test
    public void testGetAllBooks() {
        List<Book> books = new ArrayList<>();
        books.add(new Book("1984", "George Orwell", "1234567890"));
        books.add(new Book("Brave New World", "Aldous Huxley", "0987654321"));

        when(bookRepository.findAll()).thenReturn(books);

        List<Book> foundBooks = bookService.findAllBooks();
        assertNotNull(foundBooks);
        assertEquals(2, foundBooks.size());
        verify(bookRepository, times(1)).findAll();
    }

    @Test
    public void testDeleteBookById() {
        Long bookId = 1L;
        doNothing().when(bookRepository).deleteById(bookId);
        bookService.deleteBook(bookId);
        verify(bookRepository).deleteById(bookId);
    }
}

Step 3: Testing the Controller Layer

The controller tests will mock the service layer and focus on HTTP handling and routing.

package com.example.library.application;

import com.example.library.domain.model.Book;
import com.example.library.domain.service.BookService;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(BookController.class)
public class BookControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BookService bookService;

    @Test
    public void testGetBookById() throws Exception {
        Book book = new Book("1984", "George Orwell", "1234567890");
        given(bookService.findBookById(1L)).willReturn(book);

        mockMvc.perform(get("/books/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("1984"))
                .andExpect(jsonPath("$.author").value("George Orwell"))
                .andExpect(jsonPath("$.isbn").value("1234567890"));
    }

    @Test
    public void testAddBook() throws Exception {
        Book book = new Book("1984", "George Orwell", "1234567890");
        given(bookService.saveBook(any(Book.class))).willReturn(book);

        mockMvc.perform(post("/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"title\":\"1984\",\"author\":\"George Orwell\",\"isbn\":\"1234567890\"}"))
               

Clone this wiki locally