diff --git a/onion-architecture/README.md b/onion-architecture/README.md new file mode 100644 index 000000000000..2734db724df3 --- /dev/null +++ b/onion-architecture/README.md @@ -0,0 +1,171 @@ +--- +title: "Onion Architecture in Java: Building Maintainable and Scalable Applications" +shortTitle: Onion Architecture +description: "Learn how Onion architecture helps organize your code for better separation of concerns, testability, and long-term maintainability. Ideal for backend developers and software architects building enterprise applications." +category: Architectural +language: en +tag: + - Clean Architecture + - Layered Design + - Dependency Rule + - Maintainability +--- + +## Also known as + +* Ports and Adapters +* Clean Architecture (variant) + +## Intent of Onion Architecture + +The Onion Architecture aims to address common problems in traditional layered architecture by enforcing a clear separation of concerns. It centralizes domain logic and pushes infrastructure concerns to the edges, ensuring that dependencies always point inward toward the domain core. This structure promotes testability, maintainability, and flexibility in adapting to future changes. + +## Detailed Explanation of Onion Architecture with Real-World Example + +Real-world example + +> Imagine a fortress with multiple protective walls around a valuable treasure. The treasure (core domain) is heavily guarded and never exposed directly to the outside world. Visitors (like external systems or users) first pass through the outermost gates (infrastructure), where guards verify their identity. Then they proceed inward through other layers like application services, each with stricter checks, before finally reaching the treasure, but only through clearly defined and controlled pathways. Similarly, in Onion Architecture, the most critical business logic sits protected at the center. External concerns like databases, APIs, or user interfaces are kept at the outer layers, ensuring they cannot tamper directly with the core. Any interaction must pass through proper services and abstractions, preserving the domain’s integrity. + +In plain words + +> Onion Architecture builds your application like an onion: the important core (business rules) stays safe inside, while things like UI and databases are kept outside. No matter how the outer layers change, the core stays stable and unaffected. + +jeffreypalermo.com says + +> The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core. In other words, all coupling is toward the center. This architecture is unashamedly biased toward object-oriented programming, and it puts objects before all others. The Onion Architecture relies heavily on the Dependency Inversion principle. + +## Programmatic Example of Onion Architecture in Java + +The Onion Architecture in Java structures code into concentric layers where the core business logic is independent of external concerns like databases, frameworks, or UI. This is achieved by depending on abstractions rather than concrete implementations. +It ensures that the domain remains unaffected even if the technology stack changes, making applications highly maintainable and testable. + +Let's take a look at a simple `OrderService` example in an Onion Architecture style: + +1. Core Domain Layer (domain module) + +```java +public class Order { + private String orderId; + private List items; + + public Order(String orderId, List items) { + this.orderId = orderId; + this.items = items; + } + + public String getOrderId() { + return orderId; + } + + public List getItems() { + return items; + } +} +``` +2. Application Layer (application module) + +```java +public interface OrderRepository { + void save(Order order); +} +``` + +```java +public class OrderService { + private final OrderRepository orderRepository; + + public OrderService(OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + public void placeOrder(Order order) { + orderRepository.save(order); + } +} +``` +3. Infrastructure Layer (infrastructure module) + +```java +public class InMemoryOrderRepository implements OrderRepository { + private Map database = new HashMap<>(); + + @Override + public void save(Order order) { + database.put(order.getOrderId(), order); + System.out.println("Order saved: " + order.getOrderId()); + } +} +``` +4. Entry Point (App) + +```java +public class App { + public static void main(String[] args) { + OrderRepository repository = new InMemoryOrderRepository(); + OrderService service = new OrderService(repository); + + List items = Arrays.asList("Book", "Pen"); + Order order = new Order("ORD123", items); + + service.placeOrder(order); + } +} +``` + +- The Domain Layer (Order) has no dependencies. + +- The Application Layer (OrderService, OrderRepository) defines operations abstractly without worrying about how data is stored. + +- The Infrastructure Layer (InMemoryOrderRepository) provides actual data storage. + +- The App Layer wires everything together. + +## When to Use Onion Architecture in Java + +* Enterprise Applications: Perfect for large systems where business logic must remain stable despite frequent changes in technology or external dependencies. + +* Highly Maintainable Systems: Useful when you anticipate frequent feature additions or technology upgrades (e.g., switching from MySQL to MongoDB). + +* Test-Driven Development (TDD): Ideal for systems that require extensive unit and integration testing, as the domain can be tested independently. + +* Microservices: Helps keep microservices clean, with clear separation between core business rules and communication protocols like REST or gRPC. + +## Real-World Applications of Onion Architecture in Java + +* Banking and Financial Systems: Where strict control over domain rules and processes is essential, even when interfacing with different databases, APIs, or UIs. + +* E-commerce Platforms: Separates critical order, payment, and inventory logic from external services like payment gateways and user interfaces. + +* Healthcare Applications: Ensures that patient management, diagnosis, and treatment core logic stays unaffected by changes in reporting tools, hospital systems, or regulatory APIs. + +## Benefits and Trade-offs of Onion Architecture +Benefits: + +* Separation of Concerns: Clear separation between business logic and technical concerns like databases, UI, or external services. + +* High Maintainability: Core business rules can evolve independently of infrastructure or interface changes. + +* Enhanced Testability: Inner layers can be unit-tested easily without setting up external dependencies. + +* Adaptability: Easier to swap out technologies without touching the core domain. + +Trade-offs: + +* Initial Complexity: Requires careful design of layers, abstractions, and dependency rules upfront. + +* More Boilerplate Code: Interfaces, DTOs, and mappers add to codebase size and complexity. + +* Learning Curve: New developers might take longer to understand the structure if they’re not familiar with layered architecture principles. + +## Related Architectural Patterns in Java + +* [Hexagonal Architecture](https://www.geeksforgeeks.org/hexagonal-architecture-system-design/) +* [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) + +## References and Credits + +* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq) +* [Onion Architecture by Jeffery Palermo](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) +* [Onion Architecture - Medium article](https://medium.com/expedia-group-tech/onion-architecture-deed8a554423) + + diff --git a/onion-architecture/pom.xml b/onion-architecture/pom.xml new file mode 100644 index 000000000000..7d861d291b3f --- /dev/null +++ b/onion-architecture/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-core + 3.12.4 + test + + + + onion-architecture + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.iluwatar.onionarchitecture.Main + + + + + \ No newline at end of file diff --git a/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/Main.java b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/Main.java new file mode 100644 index 000000000000..7ca8a6b2011e --- /dev/null +++ b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/Main.java @@ -0,0 +1,153 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * Onion Architecture is a software design pattern that aims to promote separation of concerns by organizing code into layers that circle around a central core. The core contains the business logic, and the outer layers contain infrastructure concerns like database access, UI, and external services. + * + * 1. Domain Layer (Core) - The TodoItem class is the core domain model of the application. It represents a Todo entity and encapsulates properties like id, title, and isCompleted. This class contains the business logic related to the TodoItem, such as the method markCompleted(), which changes the state of a Todo. This class is purely business logic, and it doesn't depend on any infrastructure components like a database or UI. It’s the heart of the application. + * 2. Application Layer - The TodoService class orchestrates the use cases of the application, like creating, retrieving, completing, and deleting todos. This is the service layer responsible for the application logic. + * 3. Infrastructure Layer - The TodoRepositoryImpl is the implementation of the TodoRepository interface, which interacts with the infrastructure (in this case, an in-memory list) to persist the data. It implements the CRUD operations (add, getAll, markAsCompleted, delete) for TodoItem objects. + */ + +package com.iluwatar.onionarchitecture; + +import com.iluwatar.onionarchitecture.application.TodoService; +import com.iluwatar.onionarchitecture.domain.TodoItem; +import com.iluwatar.onionarchitecture.infrastructure.TodoRepositoryImpl; +import java.util.Scanner; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + TodoService service = new TodoService(new TodoRepositoryImpl()); + + LOGGER.info("Welcome to the TODO App!"); + + boolean running = true; + while (running) { + LOGGER.info("\nWhat would you like to do?"); + LOGGER.info("[a] Add tasks"); + LOGGER.info("[m] Mark tasks as done"); + LOGGER.info("[v] View tasks"); + LOGGER.info("[d] Delete tasks"); + LOGGER.info("[q] Quit"); + System.out.println("> "); + String choice = scanner.nextLine().trim().toLowerCase(); + + switch (choice) { + case "a": + LOGGER.info("Enter your tasks (type 'q' to stop adding):"); + while (true) { + System.out.println("> "); + String input = scanner.nextLine().trim(); + if (input.equalsIgnoreCase("q")) break; + if (!input.isEmpty()) { + service.createTodo(input); + LOGGER.debug("Task added: {}", input); + } + } + break; + + case "m": + if (service.getTodos().isEmpty()) { + LOGGER.info("📝 No tasks to mark as done."); + break; + } + LOGGER.info( + "\nEnter the ID(s) of tasks to mark as done (comma separated, or 'q' to cancel):"); + System.out.println("> "); + String idsInput = scanner.nextLine().trim(); + if (!idsInput.equalsIgnoreCase("q")) { + String[] parts = idsInput.split(","); + for (String part : parts) { + try { + int id = Integer.parseInt(part.trim()); + service.completeTodo(id); + LOGGER.info("Task with ID {} marked as done.", id); + } catch (NumberFormatException e) { + LOGGER.warn("Invalid ID: {}", part); + } + } + } + break; + + case "v": + LOGGER.info("\n📋 Your Todo List:"); + if (service.getTodos().isEmpty()) { + LOGGER.info("No tasks yet!"); + } else { + for (TodoItem item : service.getTodos()) { + String status = item.isCompleted() ? "✅ Done" : "❌ Not Done"; + LOGGER.info("{}: {} [{}]", item.getId(), item.getTitle(), status); + } + } + break; + + case "d": + if (service.getTodos().isEmpty()) { + LOGGER.info("📝 No tasks to delete."); + break; + } + LOGGER.info("\nEnter the ID(s) of tasks to delete (comma separated, or 'q' to cancel):"); + System.out.println("> "); + String idsInputForDelete = scanner.nextLine().trim(); + if (!idsInputForDelete.equalsIgnoreCase("q")) { + String[] parts = idsInputForDelete.split(","); + for (String part : parts) { + try { + int id = Integer.parseInt(part.trim()); + service.delete(id); + LOGGER.info("Task with ID {} deleted.", id); + } catch (NumberFormatException e) { + LOGGER.warn("Invalid ID: {}", part); + } + } + } + break; + + case "q": + running = false; + break; + + default: + LOGGER.warn("Unknown option. Please try again."); + } + } + + LOGGER.info("\nGoodbye! Here's your final Todo List:"); + + if (service.getTodos().size() == 0) LOGGER.info("No tasks left!"); + else { + for (TodoItem item : service.getTodos()) { + String status = item.isCompleted() ? "✅ Done" : "❌ Not Done"; + LOGGER.info("{}: {} [{}]", item.getId(), item.getTitle(), status); + } + } + + scanner.close(); + } +} diff --git a/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/application/TodoService.java b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/application/TodoService.java new file mode 100644 index 000000000000..bd81f3af9abd --- /dev/null +++ b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/application/TodoService.java @@ -0,0 +1,54 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.onionarchitecture.application; + +import com.iluwatar.onionarchitecture.domain.*; +import java.util.List; + +public class TodoService { + private final TodoRepository repository; + private static int idCounter = 1; + + public TodoService(TodoRepository repository) { + this.repository = repository; + } + + public void createTodo(String title) { + TodoItem item = new TodoItem(idCounter++, title); + repository.add(item); + } + + public List getTodos() { + return repository.getAll(); + } + + public void completeTodo(int id) { + repository.markAsCompleted(id); + } + + public void delete(int id) { + repository.delete(id); + } +} diff --git a/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoItem.java b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoItem.java new file mode 100644 index 000000000000..4deb616e9015 --- /dev/null +++ b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoItem.java @@ -0,0 +1,48 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.onionarchitecture.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class TodoItem { + private int id; + private String title; + private boolean isCompleted; + + public TodoItem(int id, String title) { + this.id = id; + this.title = title; + this.isCompleted = false; + } + + public void markCompleted() { + this.isCompleted = true; + } +} diff --git a/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoRepository.java b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoRepository.java new file mode 100644 index 000000000000..c82643d9aa91 --- /dev/null +++ b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/domain/TodoRepository.java @@ -0,0 +1,37 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.onionarchitecture.domain; + +import java.util.List; + +public interface TodoRepository { + void add(TodoItem item); + + List getAll(); + + void markAsCompleted(int id); + + void delete(int id); +} diff --git a/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/infrastructure/TodoRepositoryImpl.java b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/infrastructure/TodoRepositoryImpl.java new file mode 100644 index 000000000000..05fad3a97260 --- /dev/null +++ b/onion-architecture/src/main/java/com/iluwatar/onionarchitecture/infrastructure/TodoRepositoryImpl.java @@ -0,0 +1,58 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.onionarchitecture.infrastructure; + +import com.iluwatar.onionarchitecture.domain.*; +import java.util.ArrayList; +import java.util.List; + +public class TodoRepositoryImpl implements TodoRepository { + private final List todos = new ArrayList<>(); + + @Override + public void add(TodoItem item) { + todos.add(item); + } + + @Override + public List getAll() { + return new ArrayList<>(todos); + } + + @Override + public void markAsCompleted(int id) { + for (TodoItem item : todos) { + if (item.getId() == id) { + item.markCompleted(); + break; + } + } + } + + @Override + public void delete(int id) { + todos.removeIf(item -> item.getId() == id); + } +} diff --git a/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/application/TodoServiceTest.java b/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/application/TodoServiceTest.java new file mode 100644 index 000000000000..ab2a538e013a --- /dev/null +++ b/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/application/TodoServiceTest.java @@ -0,0 +1,129 @@ +package com.iluwatar.onionarchitecture.application; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.iluwatar.onionarchitecture.domain.TodoItem; +import com.iluwatar.onionarchitecture.domain.TodoRepository; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class TodoServiceTest { + + @Mock private TodoRepository repository; + + private TodoService todoService; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + reset(repository); + + AtomicInteger idCounter = new AtomicInteger(1); + + + Mockito.doAnswer(invocation -> { + TodoItem todo = invocation.getArgument(0); + int id = idCounter.getAndIncrement(); + todo.setId(id); + return null; + }).when(repository).add(any(TodoItem.class)); + + todoService = new TodoService(repository); + + } + + @Test + public void testCreateTodo() { + + String title = "Test Todo"; + TodoItem expectedTodo = new TodoItem(1, title); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TodoItem.class); + + todoService.createTodo(title); + + verify(repository).add(captor.capture()); + + TodoItem capturedTodo = captor.getValue(); + assertEquals(expectedTodo.getTitle(), capturedTodo.getTitle()); + assertEquals(expectedTodo.getId(), capturedTodo.getId()); +} + + @Test + public void testGetTodos() { + + TodoItem todo1 = new TodoItem(1, "Test Todo 1"); + TodoItem todo2 = new TodoItem(2, "Test Todo 2"); + List todos = List.of(todo1, todo2); + when(repository.getAll()).thenReturn(todos); + + List result = todoService.getTodos(); + + assertEquals(2, result.size()); + assertTrue(result.contains(todo1)); + assertTrue(result.contains(todo2)); + } + + @Test + public void testCompleteTodo() { + + int todoId = 1; + + todoService.completeTodo(todoId); + + verify(repository).markAsCompleted(todoId); + } + + @Test + public void testDeleteTodo() { + + int todoId = 1; + + todoService.delete(todoId); + + verify(repository).delete(todoId); + } + + @Test + public void testCreateTodoWithUniqueId() { + String title1 = "First Todo"; + String title2 = "Second Todo"; + + todoService.createTodo(title1); + todoService.createTodo(title2); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TodoItem.class); + + + verify(repository, times(2)).add(captor.capture()); + + + List capturedArgs = captor.getAllValues(); + + assertEquals(1, capturedArgs.get(0).getId()); + assertEquals(title1, capturedArgs.get(0).getTitle()); + + assertEquals(2, capturedArgs.get(1).getId()); + assertEquals(title2, capturedArgs.get(1).getTitle()); + } + + + @Test + public void testGetTodosNoItems() { + + when(repository.getAll()).thenReturn(List.of()); + + List result = todoService.getTodos(); + + assertTrue(result.isEmpty()); + } +} diff --git a/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/domain/TodoItemTest.java b/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/domain/TodoItemTest.java new file mode 100644 index 000000000000..afcf45f2c7f0 --- /dev/null +++ b/onion-architecture/src/test/java/com/iluwatar/onionarchitecture/domain/TodoItemTest.java @@ -0,0 +1,47 @@ +package com.iluwatar.onionarchitecture.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class TodoItemTest { + + @Test + public void testConstructor() { + int expectedId = 1; + String expectedTitle = "Test Todo"; + boolean expectedCompletionStatus = false; + + TodoItem todoItem = new TodoItem(expectedId, expectedTitle); + + assertEquals(expectedId, todoItem.getId()); + assertEquals(expectedTitle, todoItem.getTitle()); + assertEquals(expectedCompletionStatus, todoItem.isCompleted()); + } + + @Test + public void testMarkCompleted() { + TodoItem todoItem = new TodoItem(1, "Test Todo"); + + todoItem.markCompleted(); + + assertTrue(todoItem.isCompleted()); + } + + @Test + public void testGetters() { + int expectedId = 1; + String expectedTitle = "Test Todo"; + TodoItem todoItem = new TodoItem(expectedId, expectedTitle); + + assertEquals(expectedId, todoItem.getId()); + assertEquals(expectedTitle, todoItem.getTitle()); + } + + @Test + public void testDefaultIsCompleted() { + TodoItem todoItem = new TodoItem(1, "Test Todo"); + + assertFalse(todoItem.isCompleted()); + } +} diff --git a/pom.xml b/pom.xml index 4f5c62663d78..b3302f3e5d20 100644 --- a/pom.xml +++ b/pom.xml @@ -182,6 +182,7 @@ object-mother object-pool observer + onion-architecture optimistic-offline-lock page-controller page-object