diff --git a/README.md b/README.md
index 542b8a7..4882ba7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,65 @@
-# springboot-demo
\ No newline at end of file
+# Customer Management Service - Testing
+
+## Overview
+
+This project focuses on the implementation of robust and comprehensive testing for a REST API-based Customer Management Service. The testing framework incorporates tools like JUnit, Mockito, Testcontainers, and Jacoco to ensure reliability and maintainability.
+
+## Testing Tools and Frameworks
+- **JUnit 5:** For writing and running unit and integration tests.
+- **Mockito:** Used to mock dependencies and isolate the units being tested.
+- **Testcontainers:** Enables integration testing with a PostgreSQL database in a containerized environment.
+- **Jacoco:** Provides code coverage reporting to ensure the adequacy of test coverage.
+
+## Key Features of the Testing Suite
+
+1. **Unit Tests:**
+
+- Ensure the correctness of service and controller logic.
+Dependencies are mocked using Mockito to isolate the unit under test.
+
+**Integration Tests:**
+
+- Verify end-to-end functionality of the REST API with a real database (PostgreSQL) using Testcontainers.
+Test scenarios include CRUD operations and edge cases.
+
+3. **Code Coverage:**
+
+- Jacoco generates detailed coverage reports, ensuring all critical code paths are tested.
+
+## Running the Tests
+
+### Prerequisites:
+
+- Install Java 17+ and Maven 3.6+.
+- Ensure Docker is installed and running for Testcontainers.
+
+#### Run All Tests:
+
+```
+mvn test
+```
+Or you can manually test all the tests for the particular test file in your IDE.
+
+#### View Code Coverage:
+
+After running tests, open the generated Jacoco report in the following path:
+
+```target/site/jacoco/index.html```
+
+Navigate to this file in your browser to view detailed coverage metrics.
+
+#### Example Test Scenarios
+
+1. **Unit Test**
+- **CustomerServiceTest:**
+Tests service-layer methods for creating, updating, and deleting customers.
+Verifies exceptions like CustomerNotFoundException and CustomerEmailUnavailableException are properly thrown.
+
+2. **Integration Test**
+- **CustomerIntegrationTest**:
+Uses Testcontainers to spin up a PostgreSQL container for testing the application in a realistic environment.
+Covers scenarios like creating a customer, retrieving by ID, updating, and deleting.
+
+## Contribution
+Feel free to contribute to this project by adding more tests or enhancing existing ones. Open a pull request for any updates.
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 3d5a90e..3c71ccd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,8 +3,8 @@ services:
container_name: postgresql
image: postgres:16.2
environment:
- POSTGRES_USER: ntloc
- POSTGRES_PASSWORD: password
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
volumes:
- postgresql:/var/lib/postgresql/data
ports:
diff --git a/pom.xml b/pom.xml
index 588ef0f..accebb8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,8 @@
Demo Hello World application for Spring Boot
17
+ 22
+ 22
diff --git a/src/main/java/com/ntloc/demo/customer/Customer.java b/src/main/java/com/ntloc/demo/customer/Customer.java
index 8e5aa53..f3eb5f6 100644
--- a/src/main/java/com/ntloc/demo/customer/Customer.java
+++ b/src/main/java/com/ntloc/demo/customer/Customer.java
@@ -2,9 +2,13 @@
import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
@Table
@Entity
+@Getter
+@Setter
public class Customer {
@Id
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index e6b9347..79217cc 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -5,8 +5,8 @@ spring:
name: customer
datasource:
url: jdbc:postgresql://localhost:5432/customer
- username: ntloc
- password: password
+ username: postgres
+ password: postgres
jpa:
hibernate:
ddl-auto: create-drop
diff --git a/src/test/java/com/ntloc/demo/AbstractTestContainersTest.java b/src/test/java/com/ntloc/demo/AbstractTestContainersTest.java
new file mode 100644
index 0000000..4754840
--- /dev/null
+++ b/src/test/java/com/ntloc/demo/AbstractTestContainersTest.java
@@ -0,0 +1,25 @@
+package com.ntloc.demo;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public abstract class AbstractTestContainersTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgreSQLContainer =
+ new PostgreSQLContainer<>("postgres:16.2");
+
+ @Test
+ void canEstablishDatabaseContainerConnection() {
+ assertTrue(postgreSQLContainer.isCreated(), "Database container should be created");
+ assertTrue(postgreSQLContainer.isRunning(), "Database container should be running");
+ }
+
+}
diff --git a/src/test/java/com/ntloc/demo/AbstractTestcontainersTest.java b/src/test/java/com/ntloc/demo/AbstractTestcontainersTest.java
deleted file mode 100644
index 85db7d1..0000000
--- a/src/test/java/com/ntloc/demo/AbstractTestcontainersTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.ntloc.demo;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
-import org.testcontainers.containers.PostgreSQLContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-@Testcontainers
-public abstract class AbstractTestcontainersTest {
-
- @Container
- @ServiceConnection
- static PostgreSQLContainer> postgreSQLContainer
- = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16.2"));
-
- @Test
- void canEstablishedConnection() {
- assertThat(postgreSQLContainer.isCreated()).isTrue();
- assertThat(postgreSQLContainer.isRunning()).isTrue();
- }
-}
diff --git a/src/test/java/com/ntloc/demo/DemoApplicationTests.java b/src/test/java/com/ntloc/demo/DemoApplicationTests.java
index ab270a1..14736b5 100644
--- a/src/test/java/com/ntloc/demo/DemoApplicationTests.java
+++ b/src/test/java/com/ntloc/demo/DemoApplicationTests.java
@@ -1,10 +1,14 @@
package com.ntloc.demo;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
//@SpringBootTest
class DemoApplicationTests {
- // @Test
+// @Test
void contextLoads() {
+// System.out.println("Tests starter");
}
}
diff --git a/src/test/java/com/ntloc/demo/customer/CustomerIntegrationTest.java b/src/test/java/com/ntloc/demo/customer/CustomerIntegrationTest.java
index c85c89b..8de686b 100644
--- a/src/test/java/com/ntloc/demo/customer/CustomerIntegrationTest.java
+++ b/src/test/java/com/ntloc/demo/customer/CustomerIntegrationTest.java
@@ -1,184 +1,164 @@
package com.ntloc.demo.customer;
-import com.ntloc.demo.AbstractTestcontainersTest;
+import com.ntloc.demo.AbstractTestContainersTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-import java.util.List;
-import java.util.Objects;
+import org.springframework.http.*;
+
import java.util.UUID;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.http.HttpMethod.*;
+import static org.junit.jupiter.api.Assertions.*;
-@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-class CustomerIntegrationTest extends AbstractTestcontainersTest {
+class CustomerIntegrationTest extends AbstractTestContainersTest {
- public static final String API_CUSTOMERS_PATH = "/api/v1/customers";
+ private static final String CUSTOMER_API_PATH = "/api/v1/customers";
@Autowired
- TestRestTemplate testRestTemplate;
+ private TestRestTemplate testRestTemplate;
@Test
void shouldCreateCustomer() {
- //given
- CreateCustomerRequest request =
- new CreateCustomerRequest(
- "name",
- "email" + UUID.randomUUID() + "@gmail.com", //unique
- "address"
- );
- //when
- ResponseEntity createCustomerResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- POST,
+ // Arrange
+ CreateCustomerRequest request = new CreateCustomerRequest(
+ "John Doe",
+ "john.doe" + UUID.randomUUID() + "@gmail.com",
+ "123 Main St"
+ );
+
+ // Act
+ ResponseEntity response = testRestTemplate.exchange(
+ CUSTOMER_API_PATH,
+ HttpMethod.POST,
new HttpEntity<>(request),
- Void.class);
- //then
- assertThat(createCustomerResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
- //get all customers request
- ResponseEntity> allCustomersResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- GET,
- null,
- new ParameterizedTypeReference<>() {
- }
+ Void.class
);
- assertThat(allCustomersResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
- Customer customerCreated = Objects.requireNonNull(allCustomersResponse.getBody())
- .stream()
- .filter(c -> c.getEmail().equals(request.email()))
- .findFirst()
- .orElseThrow();
- //comparison of customer we created with create customer request
- assertThat(customerCreated.getName()).isEqualTo(request.name());
- assertThat(customerCreated.getEmail()).isEqualTo(request.email());
- assertThat(customerCreated.getAddress()).isEqualTo(request.address());
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode(), "Customer creation should return HTTP 201");
+ }
+ @Test
+ void shouldGetAllCustomers() {
+ // Arrange
+ createTestCustomer("Alice");
+ createTestCustomer("Bob");
+
+ // Act
+ ResponseEntity response = testRestTemplate.exchange(
+ CUSTOMER_API_PATH,
+ HttpMethod.GET,
+ null,
+ Customer[].class
+ );
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode(), "Get all customers should return HTTP 200");
+ assertNotNull(response.getBody(), "Customer list should not be null");
+ assertTrue(response.getBody().length >= 2, "Customer list should contain at least two customers");
}
@Test
- void shouldUpdateCustomer() {
- //given
- CreateCustomerRequest request =
- new CreateCustomerRequest(
- "name",
- "email" + UUID.randomUUID() + "@gmail.com", //unique
- "address"
- );
- ResponseEntity createCustomerResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- POST,
- new HttpEntity<>(request),
- Void.class);
- assertThat(createCustomerResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
- //get all customers request
- ResponseEntity> allCustomersResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- GET,
+ void shouldGetCustomerById() {
+ // Arrange
+ Long customerId = createAndGetCustomerId("Charlie");
+
+ // Act
+ ResponseEntity response = testRestTemplate.exchange(
+ CUSTOMER_API_PATH + "/" + customerId,
+ HttpMethod.GET,
null,
- new ParameterizedTypeReference<>() {
- }
+ Customer.class
);
- assertThat(allCustomersResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
-
- Long id = Objects.requireNonNull(allCustomersResponse.getBody()).stream()
- .filter(c -> c.getEmail().equals(request.email()))
- .map(Customer::getId)
- .findFirst()
- .orElseThrow();
- String newEmail = "newEmail" + UUID.randomUUID() + "@gmail.com";
- //when
- testRestTemplate.exchange(
- API_CUSTOMERS_PATH + "/" + id + "?email=" + newEmail,
- PUT,
- null,
- Void.class)
- .getStatusCode().is2xxSuccessful();
- //getCustomerById after we updated
- ResponseEntity customerByIdResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH + "/" + id,
- GET,
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode(), "Get customer by ID should return HTTP 200");
+ assertNotNull(response.getBody(), "Customer should not be null");
+ assertEquals("Charlie", response.getBody().getName(), "Customer name should match");
+ }
+
+ @Test
+ void shouldUpdateCustomer() {
+ // Arrange
+ Long customerId = createAndGetCustomerId("David");
+ String newName = "David Updated";
+ String newAddress = "New Address";
+
+ // Act
+ ResponseEntity response = testRestTemplate.exchange(
+ CUSTOMER_API_PATH + "/" + customerId + "?name=" + newName + "&address=" + newAddress,
+ HttpMethod.PUT,
null,
- new ParameterizedTypeReference<>() {
- }
+ Void.class
);
- assertThat(customerByIdResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
- //Do the comparison customer updated with new email we want to update
- Customer customerUpdated = Objects.requireNonNull(customerByIdResponse.getBody());
- assertThat(customerUpdated.getName()).isEqualTo(request.name());
- assertThat(customerUpdated.getEmail()).isEqualTo(newEmail);
- assertThat(customerUpdated.getAddress()).isEqualTo(request.address());
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode(), "Update customer should return HTTP 204");
+ ResponseEntity updatedCustomerResponse = testRestTemplate.exchange(
+ CUSTOMER_API_PATH + "/" + customerId,
+ HttpMethod.GET,
+ null,
+ Customer.class
+ );
+ assertNotNull(updatedCustomerResponse.getBody(), "Updated customer should not be null");
+ assertEquals(newName, updatedCustomerResponse.getBody().getName(), "Customer name should be updated");
+ assertEquals(newAddress, updatedCustomerResponse.getBody().getAddress(), "Customer address should be updated");
}
@Test
void shouldDeleteCustomer() {
- //given
- CreateCustomerRequest request =
- new CreateCustomerRequest(
- "name",
- "email" + UUID.randomUUID() + "@gmail.com", //unique
- "address"
- );
- ResponseEntity createCustomerResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- POST,
- new HttpEntity<>(request),
- Void.class);
- assertThat(createCustomerResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
- //get all customers request
- ResponseEntity> allCustomersResponse = testRestTemplate.exchange(
- API_CUSTOMERS_PATH,
- GET,
+ // Arrange
+ Long customerId = createAndGetCustomerId("Eve");
+
+ // Act
+ ResponseEntity response = testRestTemplate.exchange(
+ CUSTOMER_API_PATH + "/" + customerId,
+ HttpMethod.DELETE,
null,
- new ParameterizedTypeReference<>() {
- }
+ Void.class
);
- assertThat(allCustomersResponse.getStatusCode())
- .isEqualTo(HttpStatus.OK);
-
- Long id = Objects.requireNonNull(allCustomersResponse.getBody()).stream()
- .filter(c -> c.getEmail().equals(request.email()))
- .map(Customer::getId)
- .findFirst()
- .orElseThrow();
- //when
- testRestTemplate.exchange(
- API_CUSTOMERS_PATH + "/" + id,
- DELETE,
+
+ // Assert
+ assertEquals(HttpStatus.OK, response.getStatusCode(), "Delete customer should return HTTP 204");
+
+ ResponseEntity deletedCustomerResponse = testRestTemplate.exchange(
+ CUSTOMER_API_PATH + "/" + customerId,
+ HttpMethod.GET,
null,
- Void.class
- ).getStatusCode().is2xxSuccessful();
- //then
- //getCustomerById after we deleted that customer
- ResponseEntity