Skip to content

Commit 06f77e2

Browse files
feat(ai): Add a section on Mockito vs In-Memory Implementation for testing
1 parent cb1c771 commit 06f77e2

File tree

1 file changed

+228
-1
lines changed

1 file changed

+228
-1
lines changed

docs/recommendations.md

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,235 @@
44
//TODO;
55

66
## 2. Mockito vs In-Memory Implementation for Mocking
7-
//TODO;
7+
When writing unit tests for a service layer class(`CustomerService`), a common challenge is managing its dependencies, such as the `CustomerRepository`. We need a way to isolate the service logic from the actual data access implementation to ensure the test only validates the service's behavior. Two primary techniques for achieving this isolation are using a mocking framework like Mockito or creating an In-Memory implementation of the dependency.
8+
9+
Let's assume the following simplified interfaces and class structures:
10+
11+
```java
12+
public class Customer {
13+
private Long id;
14+
private String name;
15+
// ... constructors, getters, and setters
16+
}
17+
18+
public interface CustomerRepository {
19+
List<Customer> findAll();
20+
List<Customer> search(String query);
21+
void update(Customer customer);
22+
void delete(Long customerId);
23+
// ... other methods
24+
}
25+
26+
@Service
27+
class CustomerService {
28+
private final CustomerRepository repository;
29+
30+
public CustomerService(CustomerRepository repository) {
31+
this.repository = repository;
32+
}
33+
34+
public List<Customer> findAll() {
35+
return repository.findAll();
36+
}
37+
38+
public List<Customer> search(String query) {
39+
if (query == null || query.trim().isEmpty()) {
40+
return repository.findAll();
41+
}
42+
return repository.search(query);
43+
}
44+
45+
public void update(Customer customer) {
46+
// Assume some business logic here before calling repository
47+
repository.update(customer);
48+
}
49+
50+
public void delete(Long customerId) {
51+
repository.delete(customerId);
52+
}
53+
}
54+
```
55+
56+
Let's explore how we can test using the Mockito-based approach and the In-Memory implementation approach.
57+
58+
### Testing with Mockito Mocks
59+
This approach uses Mockito to create a mock instance of `CustomerRepository`.
60+
61+
```java
62+
import org.junit.jupiter.api.Test;
63+
import org.junit.jupiter.api.extension.ExtendWith;
64+
import org.mockito.InjectMocks;
65+
import org.mockito.Mock;
66+
import org.mockito.junit.jupiter.MockitoExtension;
67+
68+
import java.util.Arrays;
69+
import java.util.List;
70+
71+
import static org.junit.jupiter.api.Assertions.assertEquals;
72+
import static org.mockito.Mockito.*;
73+
74+
@ExtendWith(MockitoExtension.class)
75+
class CustomerServiceMockitoTest {
76+
77+
// Creates a mock instance of CustomerRepository
78+
@Mock
79+
private CustomerRepository mockRepository;
80+
81+
// Injects the mockRepository into a CustomerService instance
82+
@InjectMocks
83+
private CustomerService customerService;
84+
85+
@Test
86+
void search_ShouldReturnFilteredCustomers_WhenQueryIsProvided() {
87+
// Arrange: Setup *specific* data for this test
88+
String searchName = "Alice";
89+
List<Customer> expectedCustomers = Arrays.asList(
90+
new Customer(1L, "Alice Smith"),
91+
new Customer(2L, "Alice Johnson")
92+
);
93+
94+
// Stubbing: Program the mock to return the *expected* data
95+
// when its 'search' method is called with the specific argument.
96+
when(mockRepository.search(searchName)).thenReturn(expectedCustomers);
97+
98+
// Act
99+
List<Customer> actualCustomers = customerService.search(searchName);
100+
101+
// Assert
102+
assertEquals(2, actualCustomers.size());
103+
assertEquals("Alice Smith", actualCustomers.getFirst().getName());
104+
105+
// Verification: Ensure the dependency was called as expected
106+
verify(mockRepository, times(1)).search(searchName);
107+
verify(mockRepository, never()).findAll(); // Ensure findAll wasn't called
108+
}
109+
110+
@Test
111+
void delete_ShouldCallRepositoryDelete() {
112+
// Arrange
113+
Long customerId = 5L;
114+
115+
// Act
116+
customerService.delete(customerId);
117+
118+
// Assert/Verify
119+
// Ensure the delete method on the repository was called exactly once with the correct ID
120+
verify(mockRepository, times(1)).delete(customerId);
121+
}
122+
}
123+
```
124+
125+
**Benefits of the Mockito Approach:**
126+
127+
* **Independent Test Data Setup for Each Test:** As shown in the example, you define exactly what the mock returns (stubbing) right before the test execution. This means each test operates with a fresh, isolated set of data. A successful data setup in one test cannot affect the data or assertions of any other test.
128+
129+
* **No Extra Code to Maintain:** You do not write a second, simplified implementation of `CustomerRepository`. All the required "test behavior" is defined concisely within the test method itself using the mocking API. This significantly reduces code maintenance overhead.
130+
131+
132+
### Testing with In-Memory Implementation
133+
This approach involves creating a concrete implementation of `CustomerRepository` that stores data in simple Java collections (like `List` or `Map`) instead of connecting to a real database.
134+
135+
```java
136+
import java.util.ArrayList;
137+
import java.util.List;
138+
import java.util.Map;
139+
import java.util.concurrent.ConcurrentHashMap;
140+
import java.util.concurrent.atomic.AtomicLong;
141+
import java.util.stream.Collectors;
142+
143+
class InMemoryCustomerRepository implements CustomerRepository {
144+
// This collection holds the data for testing
145+
private final Map<Long, Customer> store = new ConcurrentHashMap<>();
146+
private final AtomicLong nextId = new AtomicLong(1);
147+
148+
public void saveInitialData(List<Customer> customers) {
149+
store.clear(); // Important: clear data before each setup
150+
customers.forEach(c -> {
151+
c.setId(nextId.getAndIncrement());
152+
store.put(c.getId(), c);
153+
});
154+
}
155+
156+
@Override
157+
public List<Customer> findAll() {
158+
return new ArrayList<>(store.values());
159+
}
160+
161+
@Override
162+
public List<Customer> search(String query) {
163+
return store.values().stream()
164+
.filter(c -> c.getName().contains(query))
165+
.collect(Collectors.toList());
166+
}
167+
168+
// Simplified implementation for the test
169+
@Override
170+
public void update(Customer customer) {
171+
store.put(customer.getId(), customer);
172+
}
173+
174+
@Override
175+
public void delete(Long customerId) {
176+
store.remove(customerId);
177+
}
178+
}
179+
```
180+
181+
The test relies on setting up the shared data structure in the `InMemoryCustomerRepository`.
182+
183+
```java
184+
import org.junit.jupiter.api.BeforeEach;
185+
import org.junit.jupiter.api.Test;
186+
import java.util.Arrays;
187+
import java.util.List;
188+
import static org.junit.jupiter.api.Assertions.assertEquals;
189+
190+
class CustomerServiceInMemoryTest {
191+
192+
private InMemoryCustomerRepository inMemoryRepository;
193+
private CustomerService customerService;
194+
195+
@BeforeEach
196+
void setup() {
197+
inMemoryRepository = new InMemoryCustomerRepository();
198+
customerService = new CustomerService(inMemoryRepository);
199+
200+
// Initial setup for ALL tests using this repository
201+
List<Customer> initialData = Arrays.asList(
202+
new Customer(null, "Charlie Brown"),
203+
new Customer(null, "Alice Smith"),
204+
new Customer(null, "David Jones")
205+
);
206+
inMemoryRepository.saveInitialData(initialData);
207+
}
208+
209+
@Test
210+
void search_ShouldReturnFilteredCustomers_WhenQueryIsProvided() {
211+
// Arrange is done in @BeforeEach, we rely on the initial data.
212+
213+
// Act
214+
List<Customer> actualCustomers = customerService.search("li");
215+
216+
// Assert
217+
assertEquals(1, actualCustomers.size());
218+
assertEquals("Charlie Brown", actualCustomers.get(0).getName());
219+
}
220+
}
221+
```
222+
223+
**Problems with the In-Memory Repository Approach:**
224+
225+
While an in-memory repository can feel more "real" than a mock, it introduces significant issues for unit testing:
226+
227+
**1. Need to Maintain Two Versions of the Repository Implementations:** You must keep the production code (e.g., Spring Data JPA implementation) and the `InMemoryCustomerRepository` perfectly compatible. Every time a method is added or the behavior of an existing method is changed (e.g., subtle ordering or filtering logic), both implementations must be updated. This creates a maintenance burden and a risk of divergence (where the in-memory version doesn't accurately reflect the production version).
228+
229+
**2. Test Execution Order Matters (Shared State Problem):** The in-memory repository keeps data in a shared collection (`List<Customer>` or `Map<Long, Customer>`). If one test modifies this shared state (e.g., a delete test), that modification persists and can affect subsequent tests. For example, a search test that relies on 5 records being present will fail if a preceding delete test removed one of them. While a `@BeforeEach` method can help reset the state, managing complex, multi-state resets quickly becomes cumbersome and brittle.
230+
231+
**3. Brittle Test Data Setup:** As the application grows and more functionality is added, maintaining a global data set for the in-memory repository becomes extremely difficult.
232+
233+
For example, a test verifying a feature expects a search query to return 23 records based on the current global data setup. If new data is added for a different test or feature, and that new data also matches the search criteria, the count might become 24. The original test's assertion (`assertEquals(23, actualCount)`) will fail, even though the service logic itself is correct. The test setup becomes hard to maintain as the application expands.
8234

235+
So, I recommend using Mockito so that you control the exact conditions of the test without the maintenance overhead and brittleness associated with managing a secondary, in-memory dependency implementation.
9236

10237
## 3. Should I aim for 100% Test Coverage?
11238
//TODO;

0 commit comments

Comments
 (0)