@corbat-tech/coding-standards-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/assets/demo.gif +0 -0
- package/dist/agent.d.ts +53 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +629 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +651 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/profiles.d.ts +39 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +526 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompts-legacy.d.ts +25 -0
- package/dist/prompts-legacy.d.ts.map +1 -0
- package/dist/prompts-legacy.js +600 -0
- package/dist/prompts-legacy.js.map +1 -0
- package/dist/prompts-v2.d.ts +30 -0
- package/dist/prompts-v2.d.ts.map +1 -0
- package/dist/prompts-v2.js +310 -0
- package/dist/prompts-v2.js.map +1 -0
- package/dist/prompts.d.ts +30 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +310 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +18 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +95 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools-legacy.d.ts +196 -0
- package/dist/tools-legacy.d.ts.map +1 -0
- package/dist/tools-legacy.js +1230 -0
- package/dist/tools-legacy.js.map +1 -0
- package/dist/tools-v2.d.ts +92 -0
- package/dist/tools-v2.d.ts.map +1 -0
- package/dist/tools-v2.js +410 -0
- package/dist/tools-v2.js.map +1 -0
- package/dist/tools.d.ts +92 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +410 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +3054 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +515 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/retry.d.ts +44 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +74 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +79 -0
- package/profiles/README.md +199 -0
- package/profiles/custom/.gitkeep +2 -0
- package/profiles/templates/_template.yaml +159 -0
- package/profiles/templates/angular.yaml +494 -0
- package/profiles/templates/java-spring-backend.yaml +512 -0
- package/profiles/templates/minimal.yaml +102 -0
- package/profiles/templates/nodejs.yaml +338 -0
- package/profiles/templates/python.yaml +340 -0
- package/profiles/templates/react.yaml +331 -0
- package/profiles/templates/vue.yaml +598 -0
- package/standards/architecture/ddd.md +173 -0
- package/standards/architecture/hexagonal.md +97 -0
- package/standards/cicd/github-actions.md +567 -0
- package/standards/clean-code/naming.md +175 -0
- package/standards/clean-code/principles.md +179 -0
- package/standards/containerization/dockerfile.md +419 -0
- package/standards/database/selection-guide.md +443 -0
- package/standards/documentation/guidelines.md +189 -0
- package/standards/event-driven/domain-events.md +527 -0
- package/standards/kubernetes/deployment.md +518 -0
- package/standards/observability/guidelines.md +665 -0
- package/standards/project-setup/initialization-checklist.md +650 -0
- package/standards/spring-boot/best-practices.md +598 -0
- package/standards/testing/guidelines.md +559 -0
- package/standards/workflow/llm-development-workflow.md +542 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# Testing Guidelines
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Testing is essential for maintainable software. These guidelines cover unit tests, integration tests, architecture tests, and testing best practices for Java/Spring Boot applications.
|
|
6
|
+
|
|
7
|
+
## Test Pyramid
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/\
|
|
11
|
+
/ \ E2E Tests (few)
|
|
12
|
+
/----\
|
|
13
|
+
/ \ Integration Tests (some)
|
|
14
|
+
/--------\
|
|
15
|
+
/ \ Unit Tests (many)
|
|
16
|
+
--------------
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Unit Tests**: 70% - Fast, isolated, test single units
|
|
20
|
+
- **Integration Tests**: 20% - Test component interactions
|
|
21
|
+
- **E2E Tests**: 10% - Test full user flows
|
|
22
|
+
|
|
23
|
+
## Test Types and Naming Conventions
|
|
24
|
+
|
|
25
|
+
| Type | Suffix | Plugin | Phase | Example |
|
|
26
|
+
|------|--------|--------|-------|---------|
|
|
27
|
+
| Unit | `*Test.java` | maven-surefire | test | `OrderServiceTest.java` |
|
|
28
|
+
| Integration | `*IT.java` | maven-failsafe | integration-test | `OrderRepositoryIT.java` |
|
|
29
|
+
| E2E | `*E2ETest.java` | maven-failsafe | integration-test | `OrderFlowE2ETest.java` |
|
|
30
|
+
| Architecture | `*ArchTest.java` | maven-surefire | test | `HexagonalArchTest.java` |
|
|
31
|
+
|
|
32
|
+
### Maven Configuration for IT Tests
|
|
33
|
+
|
|
34
|
+
```xml
|
|
35
|
+
<plugin>
|
|
36
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
37
|
+
<artifactId>maven-failsafe-plugin</artifactId>
|
|
38
|
+
<version>${maven-failsafe-plugin.version}</version>
|
|
39
|
+
<executions>
|
|
40
|
+
<execution>
|
|
41
|
+
<goals>
|
|
42
|
+
<goal>integration-test</goal>
|
|
43
|
+
<goal>verify</goal>
|
|
44
|
+
</goals>
|
|
45
|
+
</execution>
|
|
46
|
+
</executions>
|
|
47
|
+
<configuration>
|
|
48
|
+
<includes>
|
|
49
|
+
<include>**/*IT.java</include>
|
|
50
|
+
<include>**/*E2ETest.java</include>
|
|
51
|
+
</includes>
|
|
52
|
+
</configuration>
|
|
53
|
+
</plugin>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Unit Testing
|
|
57
|
+
|
|
58
|
+
### Structure: Arrange-Act-Assert (AAA)
|
|
59
|
+
|
|
60
|
+
```java
|
|
61
|
+
@Test
|
|
62
|
+
void should_CalculateTotal_When_OrderHasMultipleLines() {
|
|
63
|
+
// Arrange
|
|
64
|
+
Order order = Order.create(OrderId.generate(), customerId);
|
|
65
|
+
order.addLine(productId1, 2, Money.of(10, "USD"));
|
|
66
|
+
order.addLine(productId2, 1, Money.of(25, "USD"));
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
Money total = order.calculateTotal();
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
assertThat(total).isEqualTo(Money.of(45, "USD"));
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Test Method Naming Convention
|
|
77
|
+
|
|
78
|
+
Pattern: `should_ExpectedBehavior_When_Condition`
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
@Test
|
|
82
|
+
void should_ThrowException_When_OrderIsEmpty() { }
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
void should_ApplyDiscount_When_CustomerIsPremium() { }
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
void should_ReturnEmptyList_When_NoOrdersExist() { }
|
|
89
|
+
|
|
90
|
+
@Test
|
|
91
|
+
void should_CreateOrder_When_AllFieldsAreValid() { }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Use AssertJ for Assertions
|
|
95
|
+
|
|
96
|
+
```java
|
|
97
|
+
// Bad - JUnit assertions
|
|
98
|
+
assertEquals(expected, actual);
|
|
99
|
+
assertTrue(list.isEmpty());
|
|
100
|
+
|
|
101
|
+
// Good - AssertJ
|
|
102
|
+
assertThat(actual).isEqualTo(expected);
|
|
103
|
+
assertThat(list).isEmpty();
|
|
104
|
+
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
|
|
105
|
+
assertThat(orders)
|
|
106
|
+
.hasSize(3)
|
|
107
|
+
.extracting(Order::getStatus)
|
|
108
|
+
.containsOnly(OrderStatus.PENDING);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Testing Domain Objects
|
|
112
|
+
|
|
113
|
+
```java
|
|
114
|
+
class MoneyTest {
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
void should_CreateMoney_When_AmountIsPositive() {
|
|
118
|
+
Money money = Money.of(100, "USD");
|
|
119
|
+
|
|
120
|
+
assertThat(money.getAmount()).isEqualByComparingTo(new BigDecimal("100"));
|
|
121
|
+
assertThat(money.getCurrency()).isEqualTo(Currency.getInstance("USD"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
void should_ThrowException_When_AmountIsNegative() {
|
|
126
|
+
assertThatThrownBy(() -> Money.of(-100, "USD"))
|
|
127
|
+
.isInstanceOf(IllegalArgumentException.class)
|
|
128
|
+
.hasMessageContaining("negative");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
void should_AddMoney_When_CurrenciesMatch() {
|
|
133
|
+
Money a = Money.of(100, "USD");
|
|
134
|
+
Money b = Money.of(50, "USD");
|
|
135
|
+
|
|
136
|
+
Money result = a.add(b);
|
|
137
|
+
|
|
138
|
+
assertThat(result).isEqualTo(Money.of(150, "USD"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Mocking with Mockito
|
|
144
|
+
|
|
145
|
+
```java
|
|
146
|
+
@ExtendWith(MockitoExtension.class)
|
|
147
|
+
class PlaceOrderUseCaseTest {
|
|
148
|
+
|
|
149
|
+
@Mock
|
|
150
|
+
private OrderRepository orderRepository;
|
|
151
|
+
|
|
152
|
+
@Mock
|
|
153
|
+
private EventPublisher eventPublisher;
|
|
154
|
+
|
|
155
|
+
@InjectMocks
|
|
156
|
+
private PlaceOrderUseCase useCase;
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
void should_SaveOrder_When_CommandIsValid() {
|
|
160
|
+
// Arrange
|
|
161
|
+
PlaceOrderCommand command = new PlaceOrderCommand(customerId, lines);
|
|
162
|
+
|
|
163
|
+
// Act
|
|
164
|
+
OrderId result = useCase.execute(command);
|
|
165
|
+
|
|
166
|
+
// Assert
|
|
167
|
+
verify(orderRepository).save(any(Order.class));
|
|
168
|
+
verify(eventPublisher).publish(any(OrderPlaced.class));
|
|
169
|
+
assertThat(result).isNotNull();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test
|
|
173
|
+
void should_ThrowException_When_CustomerNotFound() {
|
|
174
|
+
// Arrange
|
|
175
|
+
when(customerRepository.findById(any())).thenReturn(Optional.empty());
|
|
176
|
+
|
|
177
|
+
// Act & Assert
|
|
178
|
+
assertThatThrownBy(() -> useCase.execute(command))
|
|
179
|
+
.isInstanceOf(CustomerNotFoundException.class);
|
|
180
|
+
|
|
181
|
+
verify(orderRepository, never()).save(any());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Integration Testing
|
|
187
|
+
|
|
188
|
+
### Naming Convention: Use `*IT.java` Suffix
|
|
189
|
+
|
|
190
|
+
Integration tests must end with `IT` for maven-failsafe automatic detection:
|
|
191
|
+
|
|
192
|
+
```java
|
|
193
|
+
// File: OrderRepositoryIT.java
|
|
194
|
+
@SpringBootTest
|
|
195
|
+
@Testcontainers
|
|
196
|
+
class OrderRepositoryIT {
|
|
197
|
+
// ...
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// File: OrderControllerIT.java
|
|
201
|
+
@SpringBootTest
|
|
202
|
+
@AutoConfigureMockMvc
|
|
203
|
+
class OrderControllerIT {
|
|
204
|
+
// ...
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Spring Boot Integration Tests
|
|
209
|
+
|
|
210
|
+
```java
|
|
211
|
+
@SpringBootTest
|
|
212
|
+
@AutoConfigureMockMvc
|
|
213
|
+
@Transactional
|
|
214
|
+
class OrderControllerIT {
|
|
215
|
+
|
|
216
|
+
@Autowired
|
|
217
|
+
private MockMvc mockMvc;
|
|
218
|
+
|
|
219
|
+
@Autowired
|
|
220
|
+
private OrderRepository orderRepository;
|
|
221
|
+
|
|
222
|
+
@Test
|
|
223
|
+
void should_CreateOrder_When_RequestIsValid() throws Exception {
|
|
224
|
+
String requestBody = """
|
|
225
|
+
{
|
|
226
|
+
"customerId": "cust-123",
|
|
227
|
+
"lines": [
|
|
228
|
+
{"productId": "prod-1", "quantity": 2}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
""";
|
|
232
|
+
|
|
233
|
+
mockMvc.perform(post("/api/v1/orders")
|
|
234
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
235
|
+
.content(requestBody))
|
|
236
|
+
.andExpect(status().isCreated())
|
|
237
|
+
.andExpect(jsonPath("$.id").exists())
|
|
238
|
+
.andExpect(jsonPath("$.status").value("PENDING"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Testcontainers for Database Tests
|
|
244
|
+
|
|
245
|
+
```java
|
|
246
|
+
@SpringBootTest
|
|
247
|
+
@Testcontainers
|
|
248
|
+
class OrderRepositoryIT {
|
|
249
|
+
|
|
250
|
+
@Container
|
|
251
|
+
static PostgreSQLContainer<?> postgres =
|
|
252
|
+
new PostgreSQLContainer<>("postgres:15-alpine");
|
|
253
|
+
|
|
254
|
+
@DynamicPropertySource
|
|
255
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
256
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
257
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
258
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Autowired
|
|
262
|
+
private OrderRepository orderRepository;
|
|
263
|
+
|
|
264
|
+
@Test
|
|
265
|
+
void should_PersistAndRetrieveOrder() {
|
|
266
|
+
Order order = createTestOrder();
|
|
267
|
+
|
|
268
|
+
orderRepository.save(order);
|
|
269
|
+
Optional<Order> found = orderRepository.findById(order.getId());
|
|
270
|
+
|
|
271
|
+
assertThat(found)
|
|
272
|
+
.isPresent()
|
|
273
|
+
.get()
|
|
274
|
+
.satisfies(o -> {
|
|
275
|
+
assertThat(o.getId()).isEqualTo(order.getId());
|
|
276
|
+
assertThat(o.getStatus()).isEqualTo(order.getStatus());
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Kafka Integration Tests
|
|
283
|
+
|
|
284
|
+
```java
|
|
285
|
+
@SpringBootTest
|
|
286
|
+
@Testcontainers
|
|
287
|
+
@EmbeddedKafka(partitions = 1, topics = {"orders.order.created"})
|
|
288
|
+
class OrderEventPublisherIT {
|
|
289
|
+
|
|
290
|
+
@Container
|
|
291
|
+
static KafkaContainer kafka = new KafkaContainer(
|
|
292
|
+
DockerImageName.parse("confluentinc/cp-kafka:7.4.0")
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
@DynamicPropertySource
|
|
296
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
297
|
+
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@Autowired
|
|
301
|
+
private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
|
|
302
|
+
|
|
303
|
+
@Test
|
|
304
|
+
void should_PublishEvent_When_OrderIsCreated() {
|
|
305
|
+
// Test Kafka integration
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Architecture Testing with ArchUnit
|
|
311
|
+
|
|
312
|
+
ArchUnit tests verify architectural constraints are respected.
|
|
313
|
+
|
|
314
|
+
### Setup
|
|
315
|
+
|
|
316
|
+
```xml
|
|
317
|
+
<dependency>
|
|
318
|
+
<groupId>com.tngtech.archunit</groupId>
|
|
319
|
+
<artifactId>archunit-junit5</artifactId>
|
|
320
|
+
<version>${archunit.version}</version>
|
|
321
|
+
<scope>test</scope>
|
|
322
|
+
</dependency>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Hexagonal Architecture Tests
|
|
326
|
+
|
|
327
|
+
```java
|
|
328
|
+
@AnalyzeClasses(packages = "com.example.myapp")
|
|
329
|
+
class HexagonalArchTest {
|
|
330
|
+
|
|
331
|
+
@ArchTest
|
|
332
|
+
static final ArchRule domain_should_not_depend_on_infrastructure =
|
|
333
|
+
noClasses()
|
|
334
|
+
.that().resideInAPackage("..domain..")
|
|
335
|
+
.should().dependOnClassesThat()
|
|
336
|
+
.resideInAPackage("..infrastructure..");
|
|
337
|
+
|
|
338
|
+
@ArchTest
|
|
339
|
+
static final ArchRule domain_should_not_depend_on_application =
|
|
340
|
+
noClasses()
|
|
341
|
+
.that().resideInAPackage("..domain..")
|
|
342
|
+
.should().dependOnClassesThat()
|
|
343
|
+
.resideInAPackage("..application..");
|
|
344
|
+
|
|
345
|
+
@ArchTest
|
|
346
|
+
static final ArchRule application_should_not_depend_on_infrastructure =
|
|
347
|
+
noClasses()
|
|
348
|
+
.that().resideInAPackage("..application..")
|
|
349
|
+
.should().dependOnClassesThat()
|
|
350
|
+
.resideInAPackage("..infrastructure..");
|
|
351
|
+
|
|
352
|
+
@ArchTest
|
|
353
|
+
static final ArchRule domain_should_not_use_spring_annotations =
|
|
354
|
+
noClasses()
|
|
355
|
+
.that().resideInAPackage("..domain..")
|
|
356
|
+
.should().dependOnClassesThat()
|
|
357
|
+
.resideInAPackage("org.springframework..");
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Naming Convention Tests
|
|
362
|
+
|
|
363
|
+
```java
|
|
364
|
+
@AnalyzeClasses(packages = "com.example.myapp")
|
|
365
|
+
class NamingConventionArchTest {
|
|
366
|
+
|
|
367
|
+
@ArchTest
|
|
368
|
+
static final ArchRule controllers_should_be_suffixed =
|
|
369
|
+
classes()
|
|
370
|
+
.that().resideInAPackage("..adapter.in.rest..")
|
|
371
|
+
.and().areAnnotatedWith(RestController.class)
|
|
372
|
+
.should().haveSimpleNameEndingWith("Controller");
|
|
373
|
+
|
|
374
|
+
@ArchTest
|
|
375
|
+
static final ArchRule use_cases_should_be_suffixed =
|
|
376
|
+
classes()
|
|
377
|
+
.that().resideInAPackage("..application.usecase..")
|
|
378
|
+
.should().haveSimpleNameEndingWith("UseCase");
|
|
379
|
+
|
|
380
|
+
@ArchTest
|
|
381
|
+
static final ArchRule repositories_should_be_suffixed =
|
|
382
|
+
classes()
|
|
383
|
+
.that().implement(Repository.class)
|
|
384
|
+
.should().haveSimpleNameEndingWith("Repository");
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Layer Definition Tests
|
|
389
|
+
|
|
390
|
+
```java
|
|
391
|
+
@AnalyzeClasses(packages = "com.example.myapp")
|
|
392
|
+
class LayeredArchitectureTest {
|
|
393
|
+
|
|
394
|
+
@ArchTest
|
|
395
|
+
static final ArchRule layer_dependencies_are_respected =
|
|
396
|
+
layeredArchitecture()
|
|
397
|
+
.consideringAllDependencies()
|
|
398
|
+
.layer("Domain").definedBy("..domain..")
|
|
399
|
+
.layer("Application").definedBy("..application..")
|
|
400
|
+
.layer("Infrastructure").definedBy("..infrastructure..")
|
|
401
|
+
|
|
402
|
+
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
|
|
403
|
+
.whereLayer("Application").mayOnlyBeAccessedByLayers("Infrastructure")
|
|
404
|
+
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Test Data
|
|
409
|
+
|
|
410
|
+
### Use Test Fixtures / Builders
|
|
411
|
+
|
|
412
|
+
```java
|
|
413
|
+
public class OrderFixtures {
|
|
414
|
+
|
|
415
|
+
public static Order aDefaultOrder() {
|
|
416
|
+
return Order.create(
|
|
417
|
+
OrderId.of("order-123"),
|
|
418
|
+
CustomerId.of("customer-456")
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public static Order anOrderWithLines(int lineCount) {
|
|
423
|
+
Order order = aDefaultOrder();
|
|
424
|
+
for (int i = 0; i < lineCount; i++) {
|
|
425
|
+
order.addLine(ProductId.of("prod-" + i), 1, Money.of(10, "USD"));
|
|
426
|
+
}
|
|
427
|
+
return order;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Use Builder Pattern for Test Data
|
|
433
|
+
|
|
434
|
+
```java
|
|
435
|
+
public class OrderTestBuilder {
|
|
436
|
+
private OrderId id = OrderId.generate();
|
|
437
|
+
private CustomerId customerId = CustomerId.of("default-customer");
|
|
438
|
+
private List<OrderLine> lines = new ArrayList<>();
|
|
439
|
+
|
|
440
|
+
public OrderTestBuilder withId(String id) {
|
|
441
|
+
this.id = OrderId.of(id);
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public OrderTestBuilder withLine(String productId, int qty, int price) {
|
|
446
|
+
lines.add(new OrderLine(ProductId.of(productId), qty, Money.of(price, "USD")));
|
|
447
|
+
return this;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public Order build() {
|
|
451
|
+
Order order = Order.create(id, customerId);
|
|
452
|
+
lines.forEach(line -> order.addLine(line.productId(), line.quantity(), line.price()));
|
|
453
|
+
return order;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Usage
|
|
458
|
+
Order order = new OrderTestBuilder()
|
|
459
|
+
.withId("order-123")
|
|
460
|
+
.withLine("prod-1", 2, 100)
|
|
461
|
+
.withLine("prod-2", 1, 50)
|
|
462
|
+
.build();
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Object Mother Pattern
|
|
466
|
+
|
|
467
|
+
```java
|
|
468
|
+
public class OrderMother {
|
|
469
|
+
|
|
470
|
+
public static Order pendingOrder() {
|
|
471
|
+
return Order.create(OrderId.generate(), CustomerId.of("customer-1"));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
public static Order confirmedOrder() {
|
|
475
|
+
Order order = pendingOrder();
|
|
476
|
+
order.confirm();
|
|
477
|
+
return order;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
public static Order shippedOrder() {
|
|
481
|
+
Order order = confirmedOrder();
|
|
482
|
+
order.ship(TrackingNumber.of("TRACK-123"));
|
|
483
|
+
return order;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Slice Tests (Spring Boot)
|
|
489
|
+
|
|
490
|
+
### Web Layer Tests
|
|
491
|
+
|
|
492
|
+
```java
|
|
493
|
+
@WebMvcTest(OrderController.class)
|
|
494
|
+
class OrderControllerTest {
|
|
495
|
+
|
|
496
|
+
@Autowired
|
|
497
|
+
private MockMvc mockMvc;
|
|
498
|
+
|
|
499
|
+
@MockBean
|
|
500
|
+
private PlaceOrderUseCase placeOrderUseCase;
|
|
501
|
+
|
|
502
|
+
@MockBean
|
|
503
|
+
private GetOrderUseCase getOrderUseCase;
|
|
504
|
+
|
|
505
|
+
@Test
|
|
506
|
+
void should_Return201_When_OrderCreated() throws Exception {
|
|
507
|
+
when(placeOrderUseCase.execute(any()))
|
|
508
|
+
.thenReturn(OrderId.of("order-123"));
|
|
509
|
+
|
|
510
|
+
mockMvc.perform(post("/api/v1/orders")
|
|
511
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
512
|
+
.content("{\"customerId\": \"cust-1\"}"))
|
|
513
|
+
.andExpect(status().isCreated());
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Repository Layer Tests
|
|
519
|
+
|
|
520
|
+
```java
|
|
521
|
+
@DataJpaTest
|
|
522
|
+
class OrderJpaRepositoryTest {
|
|
523
|
+
|
|
524
|
+
@Autowired
|
|
525
|
+
private TestEntityManager entityManager;
|
|
526
|
+
|
|
527
|
+
@Autowired
|
|
528
|
+
private OrderJpaRepository repository;
|
|
529
|
+
|
|
530
|
+
@Test
|
|
531
|
+
void should_FindOrdersByCustomer() {
|
|
532
|
+
// Arrange
|
|
533
|
+
OrderEntity order = new OrderEntity();
|
|
534
|
+
order.setCustomerId("cust-123");
|
|
535
|
+
entityManager.persist(order);
|
|
536
|
+
|
|
537
|
+
// Act
|
|
538
|
+
List<OrderEntity> found = repository.findByCustomerId("cust-123");
|
|
539
|
+
|
|
540
|
+
// Assert
|
|
541
|
+
assertThat(found).hasSize(1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Best Practices
|
|
547
|
+
|
|
548
|
+
1. **Test behavior, not implementation**: Focus on what, not how
|
|
549
|
+
2. **One assertion concept per test**: Keep tests focused
|
|
550
|
+
3. **Tests should be independent**: No shared state between tests
|
|
551
|
+
4. **Tests should be fast**: Unit tests < 100ms, integration < 5s
|
|
552
|
+
5. **Tests should be deterministic**: Same result every time
|
|
553
|
+
6. **Use meaningful test names**: They serve as documentation
|
|
554
|
+
7. **Don't test private methods**: Test through public API
|
|
555
|
+
8. **Mock external dependencies**: Database, APIs, file system
|
|
556
|
+
9. **Cover edge cases**: Null, empty, boundary values
|
|
557
|
+
10. **Maintain test code quality**: Same standards as production code
|
|
558
|
+
11. **Use `*IT.java` suffix for integration tests**: Maven Failsafe detects them automatically
|
|
559
|
+
12. **Use ArchUnit for architecture validation**: Enforce layer dependencies
|