feat: Add the implementation of Onion-Architecture (#3532)

* Add the implementation of Onion-Architecture

* Execute maven command: mvn spotless:apply
This commit is contained in:
Hattori Henzo authored and GitHub committed 2026-08-16 20:30:42 +03:00
1 parent b55fc2bcb5
commit 688095a4c7
32 files changed
+3182 -1

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>onion-architecture</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<artifactId>application</artifactId>
<name>Application</name>
<dependencies>
<dependency>
<groupId>com.iluwatar</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito for mocking -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -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.onion.application.dto;
public record PersonResponse(
Long id,
String firstName,
String lastName,
int age,
String phoneNumber,
String email,
Long categoryId,
String categoryType) {}
@@ -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.onion.application.dto;
public record SavePersonCommand(
String firstName,
String lastName,
int age,
String phoneNumber,
String email,
String address,
Long categoryId,
String categoryType) {}
@@ -0,0 +1,75 @@
/*
*
* * 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.onion.application.usecase;
import com.iluwatar.onion.application.dto.PersonResponse;
import com.iluwatar.onion.domain.repository.PersonRepository;
import java.util.Collection;
import java.util.List;
public class GetPersonUseCase {
private final PersonRepository repository;
public GetPersonUseCase(PersonRepository repository) {
this.repository = repository;
}
public PersonResponse execute(Long id) {
var person =
repository
.findById(id)
.orElseThrow(() -> new RuntimeException("Person not found with id: " + id));
return new PersonResponse(
person.getId(),
person.getFirstName(),
person.getLastName(),
person.getAge(),
person.getPhoneNumber(),
person.getEmail(),
person.getCategory().getId(),
person.getCategory().getType());
}
public List<PersonResponse> executeAll() {
return repository.findAll().stream()
.flatMap(Collection::stream)
.map(
person ->
new PersonResponse(
person.getId(),
person.getFirstName(),
person.getLastName(),
person.getAge(),
person.getPhoneNumber(),
person.getEmail(),
person.getCategory().getId(),
person.getCategory().getType()))
.toList();
}
}
@@ -0,0 +1,67 @@
/*
*
* * 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.onion.application.usecase;
import com.iluwatar.onion.application.dto.PersonResponse;
import com.iluwatar.onion.application.dto.SavePersonCommand;
import com.iluwatar.onion.domain.model.Category;
import com.iluwatar.onion.domain.model.Person;
import com.iluwatar.onion.domain.repository.PersonRepository;
public class SavePersonUseCase {
private final PersonRepository repository;
public SavePersonUseCase(PersonRepository repository) {
this.repository = repository;
}
public PersonResponse execute(SavePersonCommand command) {
var category = new Category(command.categoryId(), command.categoryType());
var person =
new Person(
null,
command.firstName(),
command.lastName(),
command.age(),
command.phoneNumber(),
command.email(),
category);
var savedPerson = repository.save(person);
return new PersonResponse(
savedPerson.getId(),
savedPerson.getFirstName(),
savedPerson.getLastName(),
savedPerson.getAge(),
savedPerson.getPhoneNumber(),
savedPerson.getEmail(),
savedPerson.getCategory().getId(),
savedPerson.getCategory().getType());
}
}
@@ -0,0 +1,193 @@
/*
*
* * 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.onion.application.usecase;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.iluwatar.onion.application.dto.PersonResponse;
import com.iluwatar.onion.domain.model.Category;
import com.iluwatar.onion.domain.model.Person;
import com.iluwatar.onion.domain.repository.PersonRepository;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class GetPersonUseCaseTest {
@Mock private PersonRepository personRepository;
private GetPersonUseCase getPersonUseCase;
@BeforeEach
void setUp() {
getPersonUseCase = new GetPersonUseCase(personRepository);
}
@Nested
@DisplayName("Execute by ID - Happy Path")
class ExecuteByIdHappyPath {
@Test
@DisplayName("Should return PersonResponse when person exists")
void shouldReturnPersonResponseWhenPersonExists() {
// Arrange
var person =
new Person(
1L,
"John",
"Doe",
25,
"+1234567890",
"john.doe@example.com",
new Category(1L, "Professional"));
when(personRepository.findById(1L)).thenReturn(Optional.of(person));
// Act
var response = getPersonUseCase.execute(1L);
// Assert
assertNotNull(response);
assertEquals(1L, response.id());
assertEquals("John", response.firstName());
assertEquals("Doe", response.lastName());
assertEquals(25, response.age());
assertEquals("+1234567890", response.phoneNumber());
assertEquals("john.doe@example.com", response.email());
assertEquals(1L, response.categoryId());
assertEquals("Professional", response.categoryType());
verify(personRepository, times(1)).findById(1L);
}
}
@Nested
@DisplayName("Execute by ID - Error Cases")
class ExecuteByIdErrorCases {
@Test
@DisplayName("Should throw RuntimeException when person not found")
void shouldThrowExceptionWhenPersonNotFound() {
// Arrange
when(personRepository.findById(999L)).thenReturn(Optional.empty());
// Act & Assert
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(999L));
assertTrue(exception.getMessage().contains("Person not found with id: 999"));
verify(personRepository, times(1)).findById(999L);
}
@Test
@DisplayName("Should propagate exception when repository throws exception")
void shouldPropagateExceptionWhenRepositoryFails() {
// Arrange
when(personRepository.findById(anyLong())).thenThrow(new RuntimeException("Database error"));
// Act & Assert
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(1L));
assertTrue(exception.getMessage().contains("Database error"));
verify(personRepository, times(1)).findById(1L);
}
}
@Nested
@DisplayName("Execute All - Happy Path")
class ExecuteAllHappyPath {
@Test
@DisplayName("Should return list of PersonResponses")
void shouldReturnListOfPersonResponses() {
// Arrange
var person1 =
new Person(
1L,
"John",
"Doe",
30,
"+9876543255",
"john.doe@example.com",
new Category(2L, "Personal"));
var person2 =
new Person(
2L,
"Jane",
"Smith",
25,
"+9876543210",
"jane.smith@example.com",
new Category(2L, "Personal"));
when(personRepository.findAll()).thenReturn(Optional.of(List.of(person1, person2)));
// Act
var responses = getPersonUseCase.executeAll();
// Assert
assertNotNull(responses);
assertEquals(2, responses.size());
PersonResponse response1 = responses.get(0);
assertEquals(1L, response1.id());
assertEquals("John", response1.firstName());
assertEquals("Doe", response1.lastName());
PersonResponse response2 = responses.get(1);
assertEquals(2L, response2.id());
assertEquals("Jane", response2.firstName());
assertEquals("Smith", response2.lastName());
verify(personRepository, times(1)).findAll();
}
@Test
@DisplayName("Should return empty list when no persons found")
void shouldReturnEmptyListWhenNoPersonsFound() {
// Arrange
when(personRepository.findAll()).thenReturn(Optional.of(List.of()));
// Act
List<PersonResponse> responses = getPersonUseCase.executeAll();
// Assert
assertNotNull(responses);
assertTrue(responses.isEmpty());
verify(personRepository, times(1)).findAll();
}
}
}
@@ -0,0 +1,253 @@
/*
*
* * 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.onion.application.usecase;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import com.iluwatar.onion.application.dto.SavePersonCommand;
import com.iluwatar.onion.domain.exception.DomainException;
import com.iluwatar.onion.domain.model.Category;
import com.iluwatar.onion.domain.model.Person;
import com.iluwatar.onion.domain.repository.PersonRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class SavePersonUseCaseTest {
@Mock private PersonRepository personRepository;
@Captor private ArgumentCaptor<Person> personCaptor;
private SavePersonUseCase savePersonUseCase;
@BeforeEach
void setUp() {
savePersonUseCase = new SavePersonUseCase(personRepository);
}
@Nested
@DisplayName("Execute - Happy Path")
class ExecuteHappyPath {
@Test
@DisplayName("Should save person and return PersonResponse")
void shouldSavePersonAndReturnResponse() {
// Arrange
var command =
new SavePersonCommand(
"John",
"Doe",
25,
"+1234567890",
"john.doe@example.com",
"123 Main St",
1L,
"Professional");
var category = new Category(1L, "Professional");
var savedPerson =
new Person(
100L, // ID generated by database
"John",
"Doe",
25,
"+1234567890",
"john.doe@example.com",
category);
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
// Act
var response = savePersonUseCase.execute(command);
// Assert
assertNotNull(response);
assertEquals(100L, response.id());
assertEquals("John", response.firstName());
assertEquals("Doe", response.lastName());
assertEquals(25, response.age());
assertEquals("+1234567890", response.phoneNumber());
assertEquals("john.doe@example.com", response.email());
assertEquals(1L, response.categoryId());
assertEquals("Professional", response.categoryType());
// Verify repository interaction
verify(personRepository, times(1)).save(any(Person.class));
}
@Test
@DisplayName("Should pass correct Person object to repository")
void shouldPassCorrectPersonToRepository() {
// Arrange
var command =
new SavePersonCommand(
"Jane",
"Smith",
30,
"+9876543210",
"jane.smith@example.com",
"456 Oak Ave",
2L,
"Personal");
var category = new Category(2L, "Personal");
var savedPerson =
new Person(200L, "Jane", "Smith", 30, "+9876543210", "jane.smith@example.com", category);
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
// Act
savePersonUseCase.execute(command);
// Assert - Verify what was passed to repository
verify(personRepository).save(personCaptor.capture());
var capturedPerson = personCaptor.getValue();
assertNull(capturedPerson.getId()); // New person should have null ID
assertEquals("Jane", capturedPerson.getFirstName());
assertEquals("Smith", capturedPerson.getLastName());
assertEquals(30, capturedPerson.getAge());
assertEquals("+9876543210", capturedPerson.getPhoneNumber());
assertEquals("jane.smith@example.com", capturedPerson.getEmail());
assertEquals("Personal", capturedPerson.getCategory().getType());
}
}
@Nested
@DisplayName("Execute - Validation Failures")
class ExecuteValidationFailures {
@Test
@DisplayName("Should throw DomainException when age is less than 18")
void shouldThrowExceptionWhenAgeIsInvalid() {
// Arrange
var command =
new SavePersonCommand(
"Young",
"Person",
17, // Invalid age
"+1234567890",
"young@example.com",
"123 Main St",
1L,
"Student");
// Act & Assert
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
// Verify repository was never called
verify(personRepository, never()).save(any(Person.class));
}
@Test
@DisplayName("Should throw DomainException when email is empty")
void shouldThrowExceptionWhenEmailIsEmpty() {
// Arrange
var command =
new SavePersonCommand(
"John",
"Doe",
25,
"+1234567890",
"", // Empty email
"123 Main St",
1L,
"Professional");
// Act & Assert
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
verify(personRepository, never()).save(any(Person.class));
}
@Test
@DisplayName("Should throw DomainException when category type is invalid")
void shouldThrowExceptionWhenCategoryTypeIsInvalid() {
// Arrange
var command =
new SavePersonCommand(
"John",
"Doe",
25,
"+1234567890",
"john@example.com",
"123 Main St",
1L,
"" // Empty category type
);
// Act & Assert
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
assertTrue(exception.getMessage().contains("Type is null or empty"));
verify(personRepository, never()).save(any(Person.class));
}
}
@Nested
@DisplayName("Execute - Repository Exceptions")
class ExecuteRepositoryExceptions {
@Test
@DisplayName("Should propagate exception when repository fails")
void shouldPropagateExceptionWhenRepositoryFails() {
// Arrange
var command =
new SavePersonCommand(
"John",
"Doe",
25,
"+1234567890",
"john.doe@example.com",
"123 Main St",
1L,
"Professional");
when(personRepository.save(any(Person.class)))
.thenThrow(new RuntimeException("Database connection failed"));
// Act & Assert
var exception =
assertThrows(RuntimeException.class, () -> savePersonUseCase.execute(command));
assertTrue(exception.getMessage().contains("Database connection failed"));
verify(personRepository, times(1)).save(any(Person.class));
}
}
}