agentic-team-templates 0.13.2 → 0.15.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/README.md +6 -1
- package/package.json +1 -1
- package/src/index.js +91 -13
- package/src/index.test.js +95 -1
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Spring Boot Patterns
|
|
2
|
+
|
|
3
|
+
Production-grade Spring Boot development. Convention over configuration, done right.
|
|
4
|
+
|
|
5
|
+
## Application Structure
|
|
6
|
+
|
|
7
|
+
```java
|
|
8
|
+
@SpringBootApplication
|
|
9
|
+
public class Application {
|
|
10
|
+
public static void main(String[] args) {
|
|
11
|
+
SpringApplication.run(Application.class, args);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// That's it. No XML. No component scanning tricks. Just this.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## REST Controllers
|
|
18
|
+
|
|
19
|
+
```java
|
|
20
|
+
@RestController
|
|
21
|
+
@RequestMapping("/api/v1/orders")
|
|
22
|
+
@RequiredArgsConstructor
|
|
23
|
+
public class OrderController {
|
|
24
|
+
|
|
25
|
+
private final OrderService orderService;
|
|
26
|
+
|
|
27
|
+
@GetMapping("/{id}")
|
|
28
|
+
public ResponseEntity<OrderResponse> getById(@PathVariable UUID id) {
|
|
29
|
+
return orderService.findById(id)
|
|
30
|
+
.map(ResponseEntity::ok)
|
|
31
|
+
.orElse(ResponseEntity.notFound().build());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@PostMapping
|
|
35
|
+
public ResponseEntity<OrderResponse> create(
|
|
36
|
+
@Valid @RequestBody CreateOrderRequest request) {
|
|
37
|
+
var order = orderService.create(request);
|
|
38
|
+
var location = URI.create("/api/v1/orders/" + order.id());
|
|
39
|
+
return ResponseEntity.created(location).body(order);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@GetMapping
|
|
43
|
+
public Page<OrderResponse> list(
|
|
44
|
+
@RequestParam(defaultValue = "0") int page,
|
|
45
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
46
|
+
return orderService.findAll(PageRequest.of(page, size));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Service Layer
|
|
52
|
+
|
|
53
|
+
```java
|
|
54
|
+
@Service
|
|
55
|
+
@RequiredArgsConstructor
|
|
56
|
+
@Transactional(readOnly = true)
|
|
57
|
+
public class OrderService {
|
|
58
|
+
|
|
59
|
+
private final OrderRepository orderRepository;
|
|
60
|
+
private final InventoryClient inventoryClient;
|
|
61
|
+
private final ApplicationEventPublisher eventPublisher;
|
|
62
|
+
|
|
63
|
+
public Optional<OrderResponse> findById(UUID id) {
|
|
64
|
+
return orderRepository.findById(id)
|
|
65
|
+
.map(OrderResponse::from);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Transactional
|
|
69
|
+
public OrderResponse create(CreateOrderRequest request) {
|
|
70
|
+
// Validate business rules
|
|
71
|
+
inventoryClient.checkAvailability(request.items())
|
|
72
|
+
.orElseThrow(() -> new InsufficientInventoryException(request.items()));
|
|
73
|
+
|
|
74
|
+
// Create and persist
|
|
75
|
+
var order = Order.create(request);
|
|
76
|
+
orderRepository.save(order);
|
|
77
|
+
|
|
78
|
+
// Publish domain event
|
|
79
|
+
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
|
|
80
|
+
|
|
81
|
+
return OrderResponse.from(order);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
```java
|
|
89
|
+
// Strongly typed configuration with validation
|
|
90
|
+
@Validated
|
|
91
|
+
@ConfigurationProperties(prefix = "app.orders")
|
|
92
|
+
public record OrderProperties(
|
|
93
|
+
@NotNull Duration processingTimeout,
|
|
94
|
+
@Min(1) @Max(1000) int maxItemsPerOrder,
|
|
95
|
+
@NotBlank String notificationEmail
|
|
96
|
+
) {}
|
|
97
|
+
|
|
98
|
+
// Enable in Application class or config
|
|
99
|
+
@EnableConfigurationProperties(OrderProperties.class)
|
|
100
|
+
|
|
101
|
+
// application.yml
|
|
102
|
+
// app:
|
|
103
|
+
// orders:
|
|
104
|
+
// processing-timeout: 30s
|
|
105
|
+
// max-items-per-order: 50
|
|
106
|
+
// notification-email: orders@example.com
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Exception Handling
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
@RestControllerAdvice
|
|
113
|
+
public class GlobalExceptionHandler {
|
|
114
|
+
|
|
115
|
+
@ExceptionHandler(NotFoundException.class)
|
|
116
|
+
public ProblemDetail handleNotFound(NotFoundException ex) {
|
|
117
|
+
var problem = ProblemDetail.forStatusAndDetail(
|
|
118
|
+
HttpStatus.NOT_FOUND, ex.getMessage());
|
|
119
|
+
problem.setTitle("Resource Not Found");
|
|
120
|
+
return problem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
124
|
+
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
|
125
|
+
var problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
|
|
126
|
+
problem.setTitle("Validation Failed");
|
|
127
|
+
var errors = ex.getBindingResult().getFieldErrors().stream()
|
|
128
|
+
.collect(Collectors.toMap(
|
|
129
|
+
FieldError::getField,
|
|
130
|
+
f -> Objects.requireNonNullElse(f.getDefaultMessage(), "Invalid")));
|
|
131
|
+
problem.setProperty("errors", errors);
|
|
132
|
+
return problem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@ExceptionHandler(Exception.class)
|
|
136
|
+
public ProblemDetail handleUnexpected(Exception ex) {
|
|
137
|
+
log.error("Unexpected error", ex);
|
|
138
|
+
return ProblemDetail.forStatusAndDetail(
|
|
139
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
140
|
+
"An unexpected error occurred");
|
|
141
|
+
// Never expose stack traces or internal details to clients
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Security
|
|
147
|
+
|
|
148
|
+
```java
|
|
149
|
+
@Configuration
|
|
150
|
+
@EnableMethodSecurity
|
|
151
|
+
public class SecurityConfig {
|
|
152
|
+
|
|
153
|
+
@Bean
|
|
154
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
155
|
+
return http
|
|
156
|
+
.csrf(csrf -> csrf.csrfTokenRepository(
|
|
157
|
+
CookieCsrfTokenRepository.withHttpOnlyFalse()))
|
|
158
|
+
.authorizeHttpRequests(auth -> auth
|
|
159
|
+
.requestMatchers("/api/public/**").permitAll()
|
|
160
|
+
.requestMatchers("/actuator/health").permitAll()
|
|
161
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
162
|
+
.anyRequest().authenticated())
|
|
163
|
+
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
|
164
|
+
.sessionManagement(session ->
|
|
165
|
+
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
166
|
+
.build();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Health Checks
|
|
172
|
+
|
|
173
|
+
```java
|
|
174
|
+
@Component
|
|
175
|
+
public class ExternalServiceHealthIndicator implements HealthIndicator {
|
|
176
|
+
|
|
177
|
+
private final ExternalServiceClient client;
|
|
178
|
+
|
|
179
|
+
@Override
|
|
180
|
+
public Health health() {
|
|
181
|
+
try {
|
|
182
|
+
client.ping();
|
|
183
|
+
return Health.up()
|
|
184
|
+
.withDetail("service", "external-api")
|
|
185
|
+
.build();
|
|
186
|
+
} catch (Exception ex) {
|
|
187
|
+
return Health.down()
|
|
188
|
+
.withDetail("service", "external-api")
|
|
189
|
+
.withException(ex)
|
|
190
|
+
.build();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// application.yml
|
|
196
|
+
// management:
|
|
197
|
+
// endpoints:
|
|
198
|
+
// web:
|
|
199
|
+
// exposure:
|
|
200
|
+
// include: health,info,prometheus
|
|
201
|
+
// endpoint:
|
|
202
|
+
// health:
|
|
203
|
+
// show-details: when-authorized
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Profiles and Environment
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
# application.yml — shared defaults
|
|
210
|
+
spring:
|
|
211
|
+
application:
|
|
212
|
+
name: order-service
|
|
213
|
+
jpa:
|
|
214
|
+
open-in-view: false # Always disable — prevents lazy loading bugs
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
# application-local.yml
|
|
218
|
+
spring:
|
|
219
|
+
config:
|
|
220
|
+
activate:
|
|
221
|
+
on-profile: local
|
|
222
|
+
datasource:
|
|
223
|
+
url: jdbc:postgresql://localhost:5432/orders
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
# application-prod.yml
|
|
227
|
+
spring:
|
|
228
|
+
config:
|
|
229
|
+
activate:
|
|
230
|
+
on-profile: prod
|
|
231
|
+
datasource:
|
|
232
|
+
url: ${DATABASE_URL} # From environment
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Anti-Patterns
|
|
236
|
+
|
|
237
|
+
```java
|
|
238
|
+
// Never: field injection
|
|
239
|
+
@Autowired
|
|
240
|
+
private OrderRepository repository; // Untestable, hides dependencies
|
|
241
|
+
// Use constructor injection (Lombok @RequiredArgsConstructor)
|
|
242
|
+
|
|
243
|
+
// Never: business logic in controllers
|
|
244
|
+
@PostMapping
|
|
245
|
+
public Order create(@RequestBody CreateOrderRequest request) {
|
|
246
|
+
var order = new Order();
|
|
247
|
+
order.setStatus("PENDING");
|
|
248
|
+
order.setItems(request.items()); // Logic belongs in service/domain
|
|
249
|
+
return orderRepository.save(order);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Never: open-in-view (lazy loading in controllers)
|
|
253
|
+
spring.jpa.open-in-view=true // N+1 queries, unclear data access patterns
|
|
254
|
+
// Set to false, use eager fetching or DTOs
|
|
255
|
+
|
|
256
|
+
// Never: catching and ignoring exceptions
|
|
257
|
+
try { externalService.call(); }
|
|
258
|
+
catch (Exception e) { /* ignore */ }
|
|
259
|
+
// Log, wrap, or rethrow — never swallow
|
|
260
|
+
|
|
261
|
+
// Never: @Transactional on private methods (Spring proxy can't intercept them)
|
|
262
|
+
```
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Java Testing
|
|
2
|
+
|
|
3
|
+
Test behavior, not implementation. Every test answers: "what does this code do?"
|
|
4
|
+
|
|
5
|
+
## Framework Stack
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| JUnit 5 | Test framework |
|
|
10
|
+
| AssertJ | Fluent assertions |
|
|
11
|
+
| Mockito | Mocking (with strict stubs) |
|
|
12
|
+
| Testcontainers | Real databases/services |
|
|
13
|
+
| Spring Boot Test | Integration testing |
|
|
14
|
+
| WireMock | HTTP API mocking |
|
|
15
|
+
| ArchUnit | Architecture tests |
|
|
16
|
+
|
|
17
|
+
## Unit Test Structure
|
|
18
|
+
|
|
19
|
+
```java
|
|
20
|
+
class OrderServiceTest {
|
|
21
|
+
|
|
22
|
+
private final OrderRepository repository = mock(OrderRepository.class);
|
|
23
|
+
private final InventoryClient inventory = mock(InventoryClient.class);
|
|
24
|
+
private final OrderService sut = new OrderService(repository, inventory);
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
void create_withValidRequest_savesAndReturnsOrder() {
|
|
28
|
+
// Arrange
|
|
29
|
+
var request = new CreateOrderRequest("customer-1", List.of(
|
|
30
|
+
new OrderItemRequest("SKU-001", 2)));
|
|
31
|
+
when(inventory.checkAvailability("SKU-001", 2)).thenReturn(true);
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
var result = sut.create(request);
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
assertThat(result.customerId()).isEqualTo("customer-1");
|
|
38
|
+
assertThat(result.status()).isEqualTo(OrderStatus.PENDING);
|
|
39
|
+
verify(repository).save(any(Order.class));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
void create_withInsufficientInventory_throwsException() {
|
|
44
|
+
var request = new CreateOrderRequest("customer-1", List.of(
|
|
45
|
+
new OrderItemRequest("SKU-001", 100)));
|
|
46
|
+
when(inventory.checkAvailability("SKU-001", 100)).thenReturn(false);
|
|
47
|
+
|
|
48
|
+
assertThatThrownBy(() -> sut.create(request))
|
|
49
|
+
.isInstanceOf(InsufficientInventoryException.class)
|
|
50
|
+
.hasMessageContaining("SKU-001");
|
|
51
|
+
|
|
52
|
+
verify(repository, never()).save(any());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Test Naming
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
// Pattern: method_scenario_expectedBehavior
|
|
61
|
+
@Test void findById_whenExists_returnsUser() {}
|
|
62
|
+
@Test void findById_whenNotFound_returnsEmpty() {}
|
|
63
|
+
@Test void create_withDuplicateEmail_throwsConflictException() {}
|
|
64
|
+
|
|
65
|
+
// Or descriptive sentences
|
|
66
|
+
@Test void newly_created_order_has_pending_status() {}
|
|
67
|
+
@Test void expired_orders_are_automatically_cancelled() {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Parameterized Tests
|
|
71
|
+
|
|
72
|
+
```java
|
|
73
|
+
@ParameterizedTest
|
|
74
|
+
@CsvSource({
|
|
75
|
+
"'', false",
|
|
76
|
+
"ab, false",
|
|
77
|
+
"abc, true",
|
|
78
|
+
"valid123, true"
|
|
79
|
+
})
|
|
80
|
+
void validatePassword_checksMinLength(String input, boolean expected) {
|
|
81
|
+
assertThat(PasswordValidator.isValid(input)).isEqualTo(expected);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@ParameterizedTest
|
|
85
|
+
@MethodSource("invalidOrderRequests")
|
|
86
|
+
void create_withInvalidData_throwsValidationException(CreateOrderRequest request) {
|
|
87
|
+
assertThatThrownBy(() -> sut.create(request))
|
|
88
|
+
.isInstanceOf(ValidationException.class);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static Stream<Arguments> invalidOrderRequests() {
|
|
92
|
+
return Stream.of(
|
|
93
|
+
Arguments.of(new CreateOrderRequest(null, List.of())),
|
|
94
|
+
Arguments.of(new CreateOrderRequest("", List.of())),
|
|
95
|
+
Arguments.of(new CreateOrderRequest("c1", List.of()))
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Spring Boot Integration Tests
|
|
101
|
+
|
|
102
|
+
```java
|
|
103
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
104
|
+
@Testcontainers
|
|
105
|
+
class OrderControllerIntegrationTest {
|
|
106
|
+
|
|
107
|
+
@Container
|
|
108
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
|
|
109
|
+
|
|
110
|
+
@DynamicPropertySource
|
|
111
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
112
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
113
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
114
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Autowired
|
|
118
|
+
private TestRestTemplate restTemplate;
|
|
119
|
+
|
|
120
|
+
@Autowired
|
|
121
|
+
private OrderRepository orderRepository;
|
|
122
|
+
|
|
123
|
+
@BeforeEach
|
|
124
|
+
void setUp() {
|
|
125
|
+
orderRepository.deleteAll();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
void postOrders_withValidRequest_returnsCreated() {
|
|
130
|
+
var request = new CreateOrderRequest("customer-1", List.of(
|
|
131
|
+
new OrderItemRequest("SKU-001", 2)));
|
|
132
|
+
|
|
133
|
+
var response = restTemplate.postForEntity("/api/v1/orders", request, OrderResponse.class);
|
|
134
|
+
|
|
135
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
136
|
+
assertThat(response.getBody()).isNotNull();
|
|
137
|
+
assertThat(response.getBody().customerId()).isEqualTo("customer-1");
|
|
138
|
+
assertThat(response.getHeaders().getLocation()).isNotNull();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
void getOrders_whenNotFound_returns404() {
|
|
143
|
+
var response = restTemplate.getForEntity(
|
|
144
|
+
"/api/v1/orders/" + UUID.randomUUID(), ProblemDetail.class);
|
|
145
|
+
|
|
146
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## WireMock for External APIs
|
|
152
|
+
|
|
153
|
+
```java
|
|
154
|
+
@SpringBootTest
|
|
155
|
+
@WireMockTest(httpPort = 8089)
|
|
156
|
+
class PaymentServiceTest {
|
|
157
|
+
|
|
158
|
+
@Autowired
|
|
159
|
+
private PaymentService paymentService;
|
|
160
|
+
|
|
161
|
+
@Test
|
|
162
|
+
void charge_whenPaymentSucceeds_returnsTransactionId() {
|
|
163
|
+
stubFor(post("/payments/charge")
|
|
164
|
+
.willReturn(okJson("""
|
|
165
|
+
{"transactionId": "tx-123", "status": "SUCCESS"}
|
|
166
|
+
""")));
|
|
167
|
+
|
|
168
|
+
var result = paymentService.charge(new ChargeRequest("order-1", BigDecimal.TEN));
|
|
169
|
+
|
|
170
|
+
assertThat(result.transactionId()).isEqualTo("tx-123");
|
|
171
|
+
verify(postRequestedFor(urlEqualTo("/payments/charge"))
|
|
172
|
+
.withRequestBody(matchingJsonPath("$.orderId", equalTo("order-1"))));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Test
|
|
176
|
+
void charge_whenPaymentServiceDown_throwsException() {
|
|
177
|
+
stubFor(post("/payments/charge")
|
|
178
|
+
.willReturn(serverError()));
|
|
179
|
+
|
|
180
|
+
assertThatThrownBy(() -> paymentService.charge(
|
|
181
|
+
new ChargeRequest("order-1", BigDecimal.TEN)))
|
|
182
|
+
.isInstanceOf(PaymentException.class);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Architecture Tests (ArchUnit)
|
|
188
|
+
|
|
189
|
+
```java
|
|
190
|
+
class ArchitectureTest {
|
|
191
|
+
|
|
192
|
+
private final JavaClasses classes = new ClassFileImporter()
|
|
193
|
+
.importPackages("com.example.myapp");
|
|
194
|
+
|
|
195
|
+
@Test
|
|
196
|
+
void domain_should_not_depend_on_infrastructure() {
|
|
197
|
+
noClasses()
|
|
198
|
+
.that().resideInAPackage("..domain..")
|
|
199
|
+
.should().dependOnClassesThat()
|
|
200
|
+
.resideInAnyPackage("..infrastructure..", "..api..")
|
|
201
|
+
.check(classes);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@Test
|
|
205
|
+
void controllers_should_not_access_repositories_directly() {
|
|
206
|
+
noClasses()
|
|
207
|
+
.that().resideInAPackage("..api..")
|
|
208
|
+
.should().dependOnClassesThat()
|
|
209
|
+
.resideInAPackage("..persistence..")
|
|
210
|
+
.check(classes);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Test
|
|
214
|
+
void services_should_be_annotated_with_service() {
|
|
215
|
+
classes()
|
|
216
|
+
.that().resideInAPackage("..application..")
|
|
217
|
+
.and().haveSimpleNameEndingWith("Service")
|
|
218
|
+
.should().beAnnotatedWith(Service.class)
|
|
219
|
+
.check(classes);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Test Data Builders
|
|
225
|
+
|
|
226
|
+
```java
|
|
227
|
+
public class TestOrders {
|
|
228
|
+
|
|
229
|
+
public static Order.Builder aValidOrder() {
|
|
230
|
+
return Order.builder()
|
|
231
|
+
.customerId("customer-1")
|
|
232
|
+
.status(OrderStatus.PENDING)
|
|
233
|
+
.items(List.of(anOrderItem().build()));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public static OrderItem.Builder anOrderItem() {
|
|
237
|
+
return OrderItem.builder()
|
|
238
|
+
.sku("SKU-" + ThreadLocalRandom.current().nextInt(1000))
|
|
239
|
+
.quantity(1)
|
|
240
|
+
.price(BigDecimal.valueOf(9.99));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Usage in tests
|
|
245
|
+
var order = TestOrders.aValidOrder()
|
|
246
|
+
.status(OrderStatus.SHIPPED)
|
|
247
|
+
.build();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Anti-Patterns
|
|
251
|
+
|
|
252
|
+
```java
|
|
253
|
+
// Never: testing implementation details
|
|
254
|
+
verify(repository, times(1)).save(any()); // How, not what
|
|
255
|
+
// Test the outcome: verify the order exists, has correct status
|
|
256
|
+
|
|
257
|
+
// Never: @SpringBootTest for unit tests (slow startup)
|
|
258
|
+
@SpringBootTest // Loads entire context for testing one service
|
|
259
|
+
class OrderServiceTest {} // Use plain JUnit + Mockito instead
|
|
260
|
+
|
|
261
|
+
// Never: Thread.sleep in tests
|
|
262
|
+
Thread.sleep(5000); // Flaky and slow
|
|
263
|
+
// Use Awaitility: await().atMost(5, SECONDS).until(() -> condition);
|
|
264
|
+
|
|
265
|
+
// Never: shared mutable state between tests
|
|
266
|
+
static List<Order> testOrders = new ArrayList<>(); // Tests pollute each other
|
|
267
|
+
// Use @BeforeEach to reset state
|
|
268
|
+
|
|
269
|
+
// Never: ignoring test failures with @Disabled without a reason
|
|
270
|
+
@Disabled // Why? When will it be fixed?
|
|
271
|
+
@Disabled("Flaky due to #1234 — external API timeout. Fix by 2025-02-01")
|
|
272
|
+
```
|