Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9e909f1
docs(readme): document the test naming convention
EdgarEldy Sep 21, 2026
9f4cb21
Merge branch 'feature/core-architecture' into feature/dao
EdgarEldy Sep 21, 2026
4491862
test(dao): rename CategoryDaoTest methods to the _NN_Should_When conv…
EdgarEldy Sep 21, 2026
cea242c
test(dao): rename CustomerDaoTest methods to the _NN_Should_When conv…
EdgarEldy Sep 21, 2026
2efba7d
test(dao): rename OrderDaoTest methods to the _NN_Should_When convention
EdgarEldy Sep 21, 2026
7031a64
test(dao): rename ProductDaoTest methods to the _NN_Should_When conve…
EdgarEldy Sep 21, 2026
07a7189
Merge branch 'feature/dao' into feature/service
EdgarEldy Sep 21, 2026
86b96bf
test(service): rename ServiceIntegrationTest methods to the _NN_Shoul…
EdgarEldy Sep 21, 2026
e374651
test(service): rename CategoryServiceImplTest methods to the _NN_Shou…
EdgarEldy Sep 21, 2026
59e153e
test(service): rename CustomerServiceImplTest methods to the _NN_Shou…
EdgarEldy Sep 21, 2026
f32e751
test(service): rename OrderReferenceGeneratorTest methods to the _NN_…
EdgarEldy Sep 21, 2026
724dcf7
test(service): rename OrderServiceImplTest methods to the _NN_Should_…
EdgarEldy Sep 21, 2026
9d5dc08
test(service): rename ProductServiceImplTest methods to the _NN_Shoul…
EdgarEldy Sep 21, 2026
9808058
test(service): rename CustomerValidatorTest methods to the _NN_Should…
EdgarEldy Sep 21, 2026
9835382
Merge branch 'feature/service' into feature/web
EdgarEldy Sep 21, 2026
c788851
test(web): rename WebIntegrationTest methods to the _NN_Should_When c…
EdgarEldy Sep 21, 2026
62c8ffb
test(web): rename CategoryControllerTest methods to the _NN_Should_Wh…
EdgarEldy Sep 21, 2026
1d81e38
test(web): rename CustomerControllerTest methods to the _NN_Should_Wh…
EdgarEldy Sep 21, 2026
4f7a768
test(web): rename OrderControllerTest methods to the _NN_Should_When …
EdgarEldy Sep 21, 2026
f899af5
test(web): rename ProductControllerTest methods to the _NN_Should_Whe…
EdgarEldy Sep 21, 2026
dcacce1
Merge pull request #1 from EdgarEldy/feature/web
EdgarEldy Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This document is the **complete specification** of the project: it is meant to b
- [Branching strategy](#branching-strategy)
- [Standard response format](#standard-response-format)
- [Spring AOP](#spring-aop)
- [Testing strategy](#testing-strategy)
- [Test naming convention](#test-naming-convention)
- [feature/core-architecture](#featurecore-architecture)
- [feature/dao](#featuredao)
- [feature/service](#featureservice)
Expand Down Expand Up @@ -216,6 +218,30 @@ The project uses **Spring AOP** to illustrate aspect-oriented programming, kept
- Also serves as a teaching base for the other advice types (`@Before`, `@After`, `@AfterReturning`, `@AfterThrowing`) alongside `@Around`
- Illustrates the proxy-based nature of Spring AOP: since services are Spring-managed beans injected by interface (`CategoryService`, `ProductService`, ...), a JDK dynamic proxy is used rather than a CGLIB subclass proxy

## Testing strategy

Every module ships its tests before its Pull Request is opened, with the tool that fits the module's place in the architecture.

| Module | Tool | What it verifies |
|---|---|---|
| `dao` | JUnit 5 + `SpringExtension` + H2 | DAO methods against a real in-memory database, with the Spring context loaded from the module's configuration |
| `service` | JUnit 5 + Mockito, and `SpringExtension` for the context tests | Business rules with the DAO layer mocked, and the wiring of the service context |
| `web` | JUnit 5 + `MockMvc` | HTTP status codes, payload shape (`ApiResponse<T>`) and error mapping, with the service layer mocked |

### Test naming convention

Every test method, in every module, is named `_NN_Should<Outcome>_When<Condition>`: a two-digit, zero-padded sequence number (the order of the methods within the class, restarting at `_01_` in each class; JUnit does not enforce it, it is kept consistent by convention), followed by what is expected, followed by the condition that produces it.

```java
@Test
void _01_ShouldReturnCategory_WhenCategoryExists() { ... }

@Test
void _02_ShouldReturnEmpty_WhenCategoryDoesNotExist() { ... }
```

No other naming style (`shouldX()`, `testX()`, `givenX_whenY_thenZ()`, `savePersistsANewCategory()`) is used anywhere in this project's test suite. This applies to test methods only, not to `@BeforeEach`/`@AfterEach` helpers.

## feature/core-architecture

Technical foundation shared by the whole project, to be merged first into `develop`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class CategoryDaoTest {
private CategoryDao categoryDao;

@Test
void savePersistsANewCategory() {
void _01_ShouldPersistCategory_WhenCategoryIsNew() {
Category category = new Category();
category.setCategoryName("Electronics");

Expand All @@ -45,14 +45,14 @@ void savePersistsANewCategory() {
}

@Test
void findByIdReturnsEmptyWhenCategoryDoesNotExist() {
void _02_ShouldReturnEmpty_WhenCategoryDoesNotExist() {
Optional<Category> found = categoryDao.findById(-1L);

assertFalse(found.isPresent());
}

@Test
void findAllReturnsSavedCategories() {
void _03_ShouldReturnSavedCategories_WhenFindingAll() {
Category category = new Category();
category.setCategoryName("Books");
categoryDao.save(category);
Expand All @@ -63,7 +63,7 @@ void findAllReturnsSavedCategories() {
}

@Test
void countReflectsSavedCategories() {
void _04_ShouldReflectSavedCategories_WhenCounting() {
long before = categoryDao.count();
Category category = new Category();
category.setCategoryName("Counted");
Expand All @@ -75,7 +75,7 @@ void countReflectsSavedCategories() {
}

@Test
void deleteByIdRemovesTheCategory() {
void _05_ShouldRemoveCategory_WhenCategoryIsDeletedById() {
Category category = new Category();
category.setCategoryName("Temporary");
Category saved = categoryDao.save(category);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class CustomerDaoTest {
private CustomerDao customerDao;

@Test
void savePersistsANewCustomer() {
void _01_ShouldPersistCustomer_WhenCustomerIsNew() {
Customer customer = new Customer();
customer.setFirstName("Grace");
customer.setLastName("Hopper");
Expand All @@ -49,14 +49,14 @@ void savePersistsANewCustomer() {
}

@Test
void findByIdReturnsEmptyWhenCustomerDoesNotExist() {
void _02_ShouldReturnEmpty_WhenCustomerDoesNotExist() {
Optional<Customer> found = customerDao.findById(-1L);

assertFalse(found.isPresent());
}

@Test
void deleteByIdRemovesTheCustomer() {
void _03_ShouldRemoveCustomer_WhenCustomerIsDeletedById() {
Customer customer = new Customer();
customer.setFirstName("Temp");
customer.setLastName("Customer");
Expand All @@ -71,7 +71,7 @@ void deleteByIdRemovesTheCustomer() {
}

@Test
void countReflectsSavedCustomers() {
void _04_ShouldReflectSavedCustomers_WhenCounting() {
long before = customerDao.count();
Customer customer = new Customer();
customer.setFirstName("Counted");
Expand All @@ -87,7 +87,7 @@ void countReflectsSavedCustomers() {
}

@Test
void findAllReturnsSavedCustomers() {
void _05_ShouldReturnSavedCustomers_WhenFindingAll() {
Customer customer = new Customer();
customer.setFirstName("Katherine");
customer.setLastName("Johnson");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ void createCustomerAndProduct() {
}

@Test
void saveAndFindByIdResolveCustomerAndProduct() {
void _01_ShouldResolveCustomerAndProduct_WhenOrderIsSavedThenFoundById() {
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
Expand All @@ -86,7 +86,7 @@ void saveAndFindByIdResolveCustomerAndProduct() {
}

@Test
void findByCustomerIdReturnsOnlyThatCustomerOrders() {
void _02_ShouldReturnOnlyThatCustomerOrders_WhenFindingByCustomerId() {
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
Expand All @@ -100,7 +100,7 @@ void findByCustomerIdReturnsOnlyThatCustomerOrders() {
}

@Test
void findByProductIdReturnsOnlyThatProductOrders() {
void _03_ShouldReturnOnlyThatProductOrders_WhenFindingByProductId() {
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
Expand All @@ -114,7 +114,7 @@ void findByProductIdReturnsOnlyThatProductOrders() {
}

@Test
void findAllReturnsSavedOrders() {
void _04_ShouldReturnSavedOrders_WhenFindingAll() {
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
Expand All @@ -128,7 +128,7 @@ void findAllReturnsSavedOrders() {
}

@Test
void countReflectsSavedOrders() {
void _05_ShouldReflectSavedOrders_WhenCounting() {
long before = orderDao.count();
Order order = new Order();
order.setCustomer(customer);
Expand All @@ -143,7 +143,7 @@ void countReflectsSavedOrders() {
}

@Test
void deleteByIdRemovesTheOrder() {
void _06_ShouldRemoveOrder_WhenOrderIsDeletedById() {
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ void createCategory() {
}

@Test
void saveAndFindByIdResolveTheCategoryRelation() {
void _01_ShouldResolveCategoryRelation_WhenProductIsSavedThenFoundById() {
Product product = new Product();
product.setCategory(category);
product.setProductName("Keyboard");
Expand All @@ -61,7 +61,7 @@ void saveAndFindByIdResolveTheCategoryRelation() {
}

@Test
void findByCategoryIdReturnsOnlyMatchingProducts() {
void _02_ShouldReturnOnlyMatchingProducts_WhenFindingByCategoryId() {
Product product = new Product();
product.setCategory(category);
product.setProductName("Mouse");
Expand All @@ -74,7 +74,7 @@ void findByCategoryIdReturnsOnlyMatchingProducts() {
}

@Test
void countByCategoryIdCountsOnlyThatCategoryProducts() {
void _03_ShouldCountOnlyThatCategoryProducts_WhenCountingByCategoryId() {
Product product = new Product();
product.setCategory(category);
product.setProductName("Monitor");
Expand All @@ -87,7 +87,7 @@ void countByCategoryIdCountsOnlyThatCategoryProducts() {
}

@Test
void findAllReturnsSavedProducts() {
void _04_ShouldReturnSavedProducts_WhenFindingAll() {
Product product = new Product();
product.setCategory(category);
product.setProductName("Webcam");
Expand All @@ -100,7 +100,7 @@ void findAllReturnsSavedProducts() {
}

@Test
void countReflectsSavedProducts() {
void _05_ShouldReflectSavedProducts_WhenCounting() {
long before = productDao.count();
Product product = new Product();
product.setCategory(category);
Expand All @@ -114,7 +114,7 @@ void countReflectsSavedProducts() {
}

@Test
void deleteByIdRemovesTheProduct() {
void _06_ShouldRemoveProduct_WhenProductIsDeletedById() {
Product product = new Product();
product.setCategory(category);
product.setProductName("Temporary");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,30 @@ class ServiceIntegrationTest {
private ObjectProvider<OrderReferenceGenerator> orderReferenceGeneratorProvider;

@Test
void paginationDefaultsResolvesTheSpelComputedDefault() {
void _01_ShouldResolveSpelComputedDefault_WhenPaginationDefaultsAreInjected() {
// #{10 * 2} evaluated by the real container, not overridden by
// ReflectionTestUtils.setField as the unit tests do.
assertEquals(20, paginationDefaults.pageSize(null));
}

@Test
void orderReferenceGeneratorIsPrototypeScoped() {
void _02_ShouldReturnDistinctInstances_WhenOrderReferenceGeneratorIsPrototypeScoped() {
OrderReferenceGenerator first = orderReferenceGeneratorProvider.getObject();
OrderReferenceGenerator second = orderReferenceGeneratorProvider.getObject();

assertNotSame(first, second);
}

@Test
void creatingAnInvalidCategoryThrowsAConstraintViolation() {
void _03_ShouldThrowConstraintViolation_WhenCategoryIsInvalid() {
Category category = new Category();
category.setCategoryName(" ");

assertThrows(ConstraintViolationException.class, () -> categoryService.create(category));
}

@Test
void categoryWithProductsCannotBeDeleted() {
void _04_ShouldRejectDeletion_WhenCategoryStillHasProducts() {
Category category = new Category();
category.setCategoryName("Electronics");
category = categoryService.create(category);
Expand All @@ -108,7 +108,7 @@ void categoryWithProductsCannotBeDeleted() {
}

@Test
void creatingAnOrderResolvesCustomerAndProductAndComputesTotal() {
void _05_ShouldResolveCustomerAndProductAndComputeTotal_WhenOrderIsCreated() {
Category category = new Category();
category.setCategoryName("Furniture");
category = categoryService.create(category);
Expand All @@ -135,7 +135,7 @@ void creatingAnOrderResolvesCustomerAndProductAndComputesTotal() {
}

@Test
void updatingAnOrderRecomputesTheTotal() {
void _06_ShouldRecomputeTotal_WhenOrderIsUpdated() {
Category category = new Category();
category.setCategoryName("Furniture");
category = categoryService.create(category);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ void setUp() {
}

@Test
void findByIdReturnsTheCategoryWhenItExists() {
void _01_ShouldReturnCategory_WhenCategoryExists() {
Category category = new Category();
category.setId(1L);
category.setCategoryName("Electronics");
Expand All @@ -60,14 +60,14 @@ void findByIdReturnsTheCategoryWhenItExists() {
}

@Test
void findByIdThrowsWhenTheCategoryDoesNotExist() {
void _02_ShouldThrowNotFound_WhenCategoryDoesNotExist() {
when(categoryDao.findById(1L)).thenReturn(Optional.empty());

assertThrows(ResourceNotFoundException.class, () -> categoryService.findById(1L));
}

@Test
void deleteRemovesTheCategoryWhenItHasNoProducts() {
void _03_ShouldRemoveCategory_WhenCategoryHasNoProducts() {
Category category = new Category();
category.setId(1L);
when(categoryDao.findById(1L)).thenReturn(Optional.of(category));
Expand All @@ -79,7 +79,7 @@ void deleteRemovesTheCategoryWhenItHasNoProducts() {
}

@Test
void deleteThrowsWhenTheCategoryStillHasProducts() {
void _04_ShouldThrowException_WhenCategoryStillHasProducts() {
Category category = new Category();
category.setId(1L);
when(categoryDao.findById(1L)).thenReturn(Optional.of(category));
Expand All @@ -91,21 +91,21 @@ void deleteThrowsWhenTheCategoryStillHasProducts() {
}

@Test
void findAllUsesTheDefaultPageSizeWhenNoneIsRequested() {
void _05_ShouldUseDefaultPageSize_WhenNoneIsRequested() {
categoryService.findAll(0, null);

verify(categoryDao).findAll(0, 20);
}

@Test
void countDelegatesToTheDao() {
void _06_ShouldDelegateToDao_WhenCounting() {
when(categoryDao.count()).thenReturn(5L);

assertEquals(5L, categoryService.count());
}

@Test
void updateChangesTheCategoryNameOfTheExistingEntity() {
void _07_ShouldChangeCategoryName_WhenExistingCategoryIsUpdated() {
Category existing = new Category();
existing.setId(1L);
existing.setCategoryName("Old name");
Expand All @@ -120,7 +120,7 @@ void updateChangesTheCategoryNameOfTheExistingEntity() {
}

@Test
void updateThrowsWhenTheCategoryDoesNotExist() {
void _08_ShouldThrowNotFound_WhenUpdatedCategoryDoesNotExist() {
when(categoryDao.findById(1L)).thenReturn(Optional.empty());
Category payload = new Category();
payload.setCategoryName("New name");
Expand Down
Loading
Loading