Skip to content

Commit 67f5e62

Browse files
authored
Merge pull request #24 from brianronock/part1-chapter12-testing
Part 1 - Chapter 12. Testing Strategies
2 parents a9bdd64 + ff227be commit 67f5e62

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.example.springrest.controllers;
2+
3+
import com.example.springrest.mappers.ProductMapperImpl;
4+
import com.example.springrest.models.Product;
5+
import com.example.springrest.services.ProductService;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
9+
import org.springframework.boot.test.mock.mockito.MockBean;
10+
import org.springframework.context.annotation.Import;
11+
import org.springframework.test.web.servlet.MockMvc;
12+
13+
import java.math.BigDecimal;
14+
15+
import static org.mockito.Mockito.when;
16+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
17+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
18+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
19+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
20+
21+
/**
22+
* Unit tests for {@link ProductController} using Spring's {@link WebMvcTest} support.
23+
*
24+
* <p><strong>Test strategy:</strong>
25+
* <ul>
26+
* <li>Start only the web layer (no database or services).</li>
27+
* <li>Inject a {@link MockMvc} client to simulate HTTP calls.</li>
28+
* <li>Replace the real {@link ProductService} with a Mockito mock using {@link MockBean}.</li>
29+
* <li>Import the {@link ProductMapperImpl} so that real mapping logic is used in tests.</li>
30+
* </ul>
31+
*
32+
* <p><strong>Note on @MockBean:</strong>
33+
* In Spring Boot 3.5+, {@link MockBean} is marked <em>deprecated</em>.
34+
* It still works, but the Spring team is encouraging a shift toward
35+
* alternative mocking approaches (e.g., plain Mockito with manual injection,
36+
* or newer testing features like {@code @ServiceTest} in future releases).
37+
* For the purposes of this book (Part 1), we continue to use {@link MockBean}
38+
* for its simplicity. We will revisit alternatives in Part 3 when covering
39+
* advanced testing strategies.
40+
*
41+
* <p><strong>Covered cases:</strong>
42+
* <ul>
43+
* <li>{@link #getByIdReturnsProduct()} — verifies that GET by ID returns the expected JSON.</li>
44+
* <li>{@link #createValidationFail()} — verifies that invalid input is rejected with 400 and error details.</li>
45+
* </ul>
46+
*/
47+
@WebMvcTest(ProductController.class)
48+
@Import(ProductMapperImpl.class)
49+
public class ProductControllerTest {
50+
@Autowired private MockMvc mvc;
51+
@MockBean private ProductService service;
52+
53+
@Test
54+
void getByIdReturnsProduct() throws Exception {
55+
Product prod = new Product("X", BigDecimal.ONE);
56+
prod.setId(100L);
57+
when(service.getOrThrow(100L)).thenReturn(prod);
58+
mvc.perform(get("/api/products/100"))
59+
.andExpect(status().isOk())
60+
.andExpect(jsonPath("$.id").value(100))
61+
.andExpect(jsonPath("$.name").value("X"));
62+
}
63+
64+
@Test
65+
void createValidationFail() throws Exception {
66+
String json = "{\"name\": \"\", \"price\": 0}";
67+
mvc.perform(post("/api/products").contentType("application/json").content(json))
68+
.andExpect(status().isBadRequest())
69+
.andExpect(jsonPath("$.name").exists())
70+
.andExpect(jsonPath("$.price").exists());
71+
}
72+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.example.springrest.repositories;
2+
3+
import com.example.springrest.models.Product;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
7+
import org.springframework.data.domain.Page;
8+
import org.springframework.data.domain.PageRequest;
9+
10+
import java.math.BigDecimal;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
14+
/**
15+
* Integration tests for {@link ProductRepo}.
16+
*
17+
* <p><strong>Responsibilities tested</strong>:
18+
* <ul>
19+
* <li>Verifies that case-insensitive search works as expected using
20+
* {@code findByNameContainingIgnoreCase}.</li>
21+
* <li>Ensures substring search returns matching results.</li>
22+
* </ul>
23+
*
24+
* <p><strong>Testing strategy</strong>:
25+
* <ul>
26+
* <li>Uses Spring Boot’s {@link DataJpaTest}
27+
* annotation to bootstrap only JPA components with an in-memory H2 database.</li>
28+
* <li>Persists test data into the repository, then queries it with different search inputs.</li>
29+
* <li>Asserts that results match expectations regardless of case or partial strings.</li>
30+
* </ul>
31+
*
32+
* <p>By default, tests are transactional and rolled back after each method,
33+
* keeping the database clean between tests.</p>
34+
*/
35+
@DataJpaTest
36+
class ProductRepoTest {
37+
38+
@Autowired
39+
ProductRepo repo;
40+
41+
@Test
42+
void searchByNameIgnoreCase() {
43+
repo.save(new Product("TestProduct", BigDecimal.TEN));
44+
repo.save(new Product("Another", BigDecimal.ONE));
45+
46+
Page<Product> page = repo.findByNameContainingIgnoreCase("testproduct", PageRequest.of(0, 10));
47+
assertEquals(1, page.getTotalElements());
48+
Product result = page.getContent().get(0);
49+
assertEquals("TestProduct", result.getName());
50+
51+
Page<Product> page2 = repo.findByNameContainingIgnoreCase("tes", PageRequest.of(0, 10));
52+
assertEquals(1, page2.getTotalElements());
53+
}
54+
55+
56+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.example.springrest.services;
2+
3+
import com.example.springrest.exceptions.ResourceNotFoundException;
4+
import com.example.springrest.models.Product;
5+
import com.example.springrest.repositories.ProductRepo;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.mockito.InjectMocks;
9+
import org.mockito.Mock;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
12+
import java.math.BigDecimal;
13+
import java.util.Optional;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
import static org.mockito.Mockito.verify;
18+
import static org.mockito.Mockito.when;
19+
20+
/**
21+
* Unit tests for {@link ProductService}.
22+
*
23+
* <p><strong>Responsibilities tested</strong>:
24+
* <ul>
25+
* <li>Ensures that {@code getOrThrow} correctly raises a
26+
* {@link ResourceNotFoundException}
27+
* when a product is not found in the repository.</li>
28+
* <li>Verifies that {@code update} applies field changes
29+
* (e.g., updating a product’s price) and persists them via the repository.</li>
30+
* </ul>
31+
*
32+
* <p><strong>Testing strategy</strong>:
33+
* <ul>
34+
* <li>Uses Mockito to mock {@link ProductRepo}
35+
* and inject it into the service under test.</li>
36+
* <li>Focuses purely on service logic, isolated from database or web concerns.</li>
37+
* <li>Asserts both behavior (exceptions thrown) and state changes
38+
* (updated price, repository interactions).</li>
39+
* </ul>
40+
*/
41+
@ExtendWith(MockitoExtension.class)
42+
public class ProductServiceTest {
43+
44+
@Mock
45+
ProductRepo repo;
46+
@InjectMocks
47+
ProductService service;
48+
49+
@Test
50+
void getOrThrowsIfNotFound() {
51+
when(repo.findById(42L)).thenReturn(Optional.empty());
52+
assertThrows(ResourceNotFoundException.class, () -> service.getOrThrow(42L));
53+
}
54+
55+
@Test
56+
void updateChangesFields() {
57+
Product existing = new Product("Old", BigDecimal.valueOf(5));
58+
existing.setId(1L);
59+
when(repo.findById(1L)).thenReturn(Optional.of(existing));
60+
when(repo.save(existing)).thenReturn(existing);
61+
service.update(1L, p -> p.setPrice(BigDecimal.TEN));
62+
assertEquals(BigDecimal.TEN, existing.getPrice());
63+
verify(repo).save(existing);
64+
}
65+
}

0 commit comments

Comments
 (0)