autoworkflow 3.1.4 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Java Skill
|
|
2
|
+
|
|
3
|
+
## Records (Java 16+)
|
|
4
|
+
\`\`\`java
|
|
5
|
+
// Immutable data carriers
|
|
6
|
+
public record User(
|
|
7
|
+
String id,
|
|
8
|
+
String email,
|
|
9
|
+
String name,
|
|
10
|
+
boolean isActive,
|
|
11
|
+
Instant createdAt
|
|
12
|
+
) {
|
|
13
|
+
// Compact constructor for validation
|
|
14
|
+
public User {
|
|
15
|
+
Objects.requireNonNull(id, "id cannot be null");
|
|
16
|
+
Objects.requireNonNull(email, "email cannot be null");
|
|
17
|
+
if (!email.contains("@")) {
|
|
18
|
+
throw new IllegalArgumentException("Invalid email format");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Additional methods
|
|
23
|
+
public String displayName() {
|
|
24
|
+
return name != null ? name : email.split("@")[0];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// DTOs as records
|
|
29
|
+
public record CreateUserRequest(
|
|
30
|
+
@NotBlank String email,
|
|
31
|
+
@NotBlank @Size(min = 2, max = 100) String name,
|
|
32
|
+
@NotBlank @Size(min = 8) String password
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
public record UserResponse(
|
|
36
|
+
String id,
|
|
37
|
+
String email,
|
|
38
|
+
String name,
|
|
39
|
+
boolean isActive
|
|
40
|
+
) {
|
|
41
|
+
public static UserResponse from(User user) {
|
|
42
|
+
return new UserResponse(
|
|
43
|
+
user.id(),
|
|
44
|
+
user.email(),
|
|
45
|
+
user.name(),
|
|
46
|
+
user.isActive()
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public record PaginatedResponse<T>(
|
|
52
|
+
List<T> items,
|
|
53
|
+
long total,
|
|
54
|
+
int page,
|
|
55
|
+
int perPage
|
|
56
|
+
) {
|
|
57
|
+
public int totalPages() {
|
|
58
|
+
return (int) Math.ceil((double) total / perPage);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public boolean hasNext() {
|
|
62
|
+
return page < totalPages();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
## Optional Patterns
|
|
68
|
+
\`\`\`java
|
|
69
|
+
import java.util.Optional;
|
|
70
|
+
|
|
71
|
+
public class UserService {
|
|
72
|
+
private final UserRepository repository;
|
|
73
|
+
|
|
74
|
+
// Return Optional for queries that may not find a result
|
|
75
|
+
public Optional<User> findById(String id) {
|
|
76
|
+
return repository.findById(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Use Optional methods instead of null checks
|
|
80
|
+
public User getUser(String id) {
|
|
81
|
+
return findById(id)
|
|
82
|
+
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Chaining Optional operations
|
|
86
|
+
public Optional<String> getUserEmail(String id) {
|
|
87
|
+
return findById(id)
|
|
88
|
+
.filter(User::isActive)
|
|
89
|
+
.map(User::email);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Optional with default value
|
|
93
|
+
public String getUserName(String id) {
|
|
94
|
+
return findById(id)
|
|
95
|
+
.map(User::name)
|
|
96
|
+
.orElse("Anonymous");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Optional with lazy default
|
|
100
|
+
public User getOrCreate(String email) {
|
|
101
|
+
return repository.findByEmail(email)
|
|
102
|
+
.orElseGet(() -> repository.save(new User(email)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Avoid Optional.get() - use orElseThrow instead
|
|
106
|
+
public void processUser(String id) {
|
|
107
|
+
findById(id).ifPresentOrElse(
|
|
108
|
+
this::process,
|
|
109
|
+
() -> log.warn("User not found: {}", id)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Flatten nested Optionals
|
|
114
|
+
public Optional<String> getProfileBio(String userId) {
|
|
115
|
+
return findById(userId)
|
|
116
|
+
.flatMap(User::profile) // profile returns Optional<Profile>
|
|
117
|
+
.map(Profile::bio);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
## Stream API
|
|
123
|
+
\`\`\`java
|
|
124
|
+
import java.util.stream.*;
|
|
125
|
+
|
|
126
|
+
public class UserAnalytics {
|
|
127
|
+
|
|
128
|
+
// Filter, map, collect
|
|
129
|
+
public List<String> getActiveUserEmails(List<User> users) {
|
|
130
|
+
return users.stream()
|
|
131
|
+
.filter(User::isActive)
|
|
132
|
+
.map(User::email)
|
|
133
|
+
.sorted()
|
|
134
|
+
.toList(); // Java 16+ (or .collect(Collectors.toList()))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Grouping
|
|
138
|
+
public Map<String, List<User>> groupByDomain(List<User> users) {
|
|
139
|
+
return users.stream()
|
|
140
|
+
.collect(Collectors.groupingBy(
|
|
141
|
+
user -> user.email().split("@")[1]
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Counting
|
|
146
|
+
public Map<Boolean, Long> countByActiveStatus(List<User> users) {
|
|
147
|
+
return users.stream()
|
|
148
|
+
.collect(Collectors.partitioningBy(
|
|
149
|
+
User::isActive,
|
|
150
|
+
Collectors.counting()
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Reducing
|
|
155
|
+
public int totalPostCount(List<User> users) {
|
|
156
|
+
return users.stream()
|
|
157
|
+
.mapToInt(User::postCount)
|
|
158
|
+
.sum();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// FlatMap for nested collections
|
|
162
|
+
public List<Post> getAllPosts(List<User> users) {
|
|
163
|
+
return users.stream()
|
|
164
|
+
.flatMap(user -> user.posts().stream())
|
|
165
|
+
.sorted(Comparator.comparing(Post::createdAt).reversed())
|
|
166
|
+
.toList();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Finding elements
|
|
170
|
+
public Optional<User> findFirstAdmin(List<User> users) {
|
|
171
|
+
return users.stream()
|
|
172
|
+
.filter(u -> u.role().equals("ADMIN"))
|
|
173
|
+
.findFirst();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Checking conditions
|
|
177
|
+
public boolean allUsersActive(List<User> users) {
|
|
178
|
+
return users.stream().allMatch(User::isActive);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public boolean anyUserAdmin(List<User> users) {
|
|
182
|
+
return users.stream().anyMatch(u -> u.role().equals("ADMIN"));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Parallel streams (use for CPU-intensive operations on large datasets)
|
|
186
|
+
public List<UserStats> computeStats(List<User> users) {
|
|
187
|
+
return users.parallelStream()
|
|
188
|
+
.map(this::computeUserStats)
|
|
189
|
+
.toList();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Collectors.toMap
|
|
193
|
+
public Map<String, User> userById(List<User> users) {
|
|
194
|
+
return users.stream()
|
|
195
|
+
.collect(Collectors.toMap(
|
|
196
|
+
User::id,
|
|
197
|
+
Function.identity(),
|
|
198
|
+
(existing, replacement) -> existing // Handle duplicates
|
|
199
|
+
));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## Exception Handling
|
|
205
|
+
\`\`\`java
|
|
206
|
+
// Custom exceptions
|
|
207
|
+
public class AppException extends RuntimeException {
|
|
208
|
+
private final ErrorCode code;
|
|
209
|
+
|
|
210
|
+
public AppException(ErrorCode code, String message) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.code = code;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public AppException(ErrorCode code, String message, Throwable cause) {
|
|
216
|
+
super(message, cause);
|
|
217
|
+
this.code = code;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public ErrorCode getCode() {
|
|
221
|
+
return code;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public class NotFoundException extends AppException {
|
|
226
|
+
public NotFoundException(String message) {
|
|
227
|
+
super(ErrorCode.NOT_FOUND, message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public class ValidationException extends AppException {
|
|
232
|
+
private final Map<String, String> errors;
|
|
233
|
+
|
|
234
|
+
public ValidationException(Map<String, String> errors) {
|
|
235
|
+
super(ErrorCode.VALIDATION_ERROR, "Validation failed");
|
|
236
|
+
this.errors = errors;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public Map<String, String> getErrors() {
|
|
240
|
+
return errors;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try-with-resources
|
|
245
|
+
public void processFile(Path path) {
|
|
246
|
+
try (var reader = Files.newBufferedReader(path);
|
|
247
|
+
var writer = Files.newBufferedWriter(outputPath)) {
|
|
248
|
+
// Resources automatically closed
|
|
249
|
+
reader.lines()
|
|
250
|
+
.map(this::transform)
|
|
251
|
+
.forEach(line -> {
|
|
252
|
+
try {
|
|
253
|
+
writer.write(line);
|
|
254
|
+
writer.newLine();
|
|
255
|
+
} catch (IOException e) {
|
|
256
|
+
throw new UncheckedIOException(e);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
} catch (IOException e) {
|
|
260
|
+
throw new AppException(ErrorCode.IO_ERROR, "Failed to process file", e);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Wrapping checked exceptions
|
|
265
|
+
public User parseUser(String json) {
|
|
266
|
+
try {
|
|
267
|
+
return objectMapper.readValue(json, User.class);
|
|
268
|
+
} catch (JsonProcessingException e) {
|
|
269
|
+
throw new AppException(ErrorCode.PARSE_ERROR, "Invalid JSON", e);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
\`\`\`
|
|
273
|
+
|
|
274
|
+
## Collections
|
|
275
|
+
\`\`\`java
|
|
276
|
+
import java.util.*;
|
|
277
|
+
|
|
278
|
+
// Immutable collections (Java 9+)
|
|
279
|
+
List<String> names = List.of("Alice", "Bob", "Charlie");
|
|
280
|
+
Set<Integer> numbers = Set.of(1, 2, 3);
|
|
281
|
+
Map<String, Integer> scores = Map.of("Alice", 100, "Bob", 85);
|
|
282
|
+
|
|
283
|
+
// Mutable when needed
|
|
284
|
+
List<String> mutableList = new ArrayList<>(names);
|
|
285
|
+
mutableList.add("David");
|
|
286
|
+
|
|
287
|
+
// Map operations
|
|
288
|
+
Map<String, User> userCache = new HashMap<>();
|
|
289
|
+
|
|
290
|
+
// computeIfAbsent - lazy initialization
|
|
291
|
+
User user = userCache.computeIfAbsent(userId, id -> repository.findById(id).orElse(null));
|
|
292
|
+
|
|
293
|
+
// getOrDefault
|
|
294
|
+
int count = countMap.getOrDefault(key, 0);
|
|
295
|
+
|
|
296
|
+
// merge - accumulating values
|
|
297
|
+
wordCounts.merge(word, 1, Integer::sum);
|
|
298
|
+
|
|
299
|
+
// Concurrent collections
|
|
300
|
+
Map<String, User> concurrentCache = new ConcurrentHashMap<>();
|
|
301
|
+
Queue<Task> taskQueue = new ConcurrentLinkedQueue<>();
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## Testing with JUnit 5
|
|
305
|
+
\`\`\`java
|
|
306
|
+
import org.junit.jupiter.api.*;
|
|
307
|
+
import org.junit.jupiter.params.ParameterizedTest;
|
|
308
|
+
import org.junit.jupiter.params.provider.*;
|
|
309
|
+
import static org.assertj.core.api.Assertions.*;
|
|
310
|
+
import static org.mockito.Mockito.*;
|
|
311
|
+
|
|
312
|
+
class UserServiceTest {
|
|
313
|
+
private UserRepository repository;
|
|
314
|
+
private UserService service;
|
|
315
|
+
|
|
316
|
+
@BeforeEach
|
|
317
|
+
void setUp() {
|
|
318
|
+
repository = mock(UserRepository.class);
|
|
319
|
+
service = new UserService(repository);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
@DisplayName("findById returns user when exists")
|
|
324
|
+
void findById_whenUserExists_returnsUser() {
|
|
325
|
+
var user = new User("1", "test@example.com", "Test", true, Instant.now());
|
|
326
|
+
when(repository.findById("1")).thenReturn(Optional.of(user));
|
|
327
|
+
|
|
328
|
+
var result = service.findById("1");
|
|
329
|
+
|
|
330
|
+
assertThat(result).isPresent();
|
|
331
|
+
assertThat(result.get().email()).isEqualTo("test@example.com");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@Test
|
|
335
|
+
@DisplayName("getUser throws NotFoundException when not exists")
|
|
336
|
+
void getUser_whenNotExists_throwsException() {
|
|
337
|
+
when(repository.findById("999")).thenReturn(Optional.empty());
|
|
338
|
+
|
|
339
|
+
assertThatThrownBy(() -> service.getUser("999"))
|
|
340
|
+
.isInstanceOf(NotFoundException.class)
|
|
341
|
+
.hasMessageContaining("999");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@ParameterizedTest
|
|
345
|
+
@ValueSource(strings = {"", " ", "invalid-email"})
|
|
346
|
+
@DisplayName("validateEmail rejects invalid emails")
|
|
347
|
+
void validateEmail_withInvalidEmails_returnsFalse(String email) {
|
|
348
|
+
assertThat(service.validateEmail(email)).isFalse();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@ParameterizedTest
|
|
352
|
+
@CsvSource({
|
|
353
|
+
"test@example.com, true",
|
|
354
|
+
"user@domain.org, true",
|
|
355
|
+
"invalid, false",
|
|
356
|
+
", false"
|
|
357
|
+
})
|
|
358
|
+
void validateEmail_withVariousInputs(String email, boolean expected) {
|
|
359
|
+
assertThat(service.validateEmail(email)).isEqualTo(expected);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@Test
|
|
363
|
+
void createUser_savesAndReturnsUser() {
|
|
364
|
+
var request = new CreateUserRequest("test@example.com", "Test", "password123");
|
|
365
|
+
when(repository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0));
|
|
366
|
+
|
|
367
|
+
var result = service.createUser(request);
|
|
368
|
+
|
|
369
|
+
assertThat(result.email()).isEqualTo("test@example.com");
|
|
370
|
+
verify(repository).save(any(User.class));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@Nested
|
|
374
|
+
@DisplayName("when user is admin")
|
|
375
|
+
class WhenAdmin {
|
|
376
|
+
@Test
|
|
377
|
+
void canDeleteOtherUsers() {
|
|
378
|
+
// ...
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
\`\`\`
|
|
383
|
+
|
|
384
|
+
## ✅ DO
|
|
385
|
+
- Use records for immutable data carriers (DTOs, value objects)
|
|
386
|
+
- Use Optional for return types that may have no value
|
|
387
|
+
- Use streams for collection transformations
|
|
388
|
+
- Use var for local variables with obvious types
|
|
389
|
+
- Use try-with-resources for AutoCloseable resources
|
|
390
|
+
- Prefer List.of(), Set.of(), Map.of() for immutable collections
|
|
391
|
+
- Use @DisplayName for readable test names
|
|
392
|
+
|
|
393
|
+
## ❌ DON'T
|
|
394
|
+
- Don't use Optional.get() - use orElseThrow() instead
|
|
395
|
+
- Don't use Optional as field types or method parameters
|
|
396
|
+
- Don't use null when Optional is appropriate
|
|
397
|
+
- Don't mutate collections during stream operations
|
|
398
|
+
- Don't use raw types (List instead of List<String>)
|
|
399
|
+
- Don't catch Exception or Throwable generically
|
|
400
|
+
- Don't use parallel streams for I/O operations
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Jest Skill
|
|
2
|
+
|
|
3
|
+
## Test Structure
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
describe('UserService', () => {
|
|
6
|
+
let service: UserService;
|
|
7
|
+
let mockDb: jest.Mocked<Database>;
|
|
8
|
+
|
|
9
|
+
// Setup before each test
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockDb = { findUser: jest.fn(), saveUser: jest.fn() } as any;
|
|
12
|
+
service = new UserService(mockDb);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Cleanup after each test
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getUser', () => {
|
|
21
|
+
it('should return user when found', async () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
mockDb.findUser.mockResolvedValue({ id: '1', name: 'Test' });
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const result = await service.getUser('1');
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(result).toEqual({ id: '1', name: 'Test' });
|
|
30
|
+
expect(mockDb.findUser).toHaveBeenCalledWith('1');
|
|
31
|
+
expect(mockDb.findUser).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw when user not found', async () => {
|
|
35
|
+
mockDb.findUser.mockResolvedValue(null);
|
|
36
|
+
|
|
37
|
+
await expect(service.getUser('1')).rejects.toThrow('Not found');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
## Mocking
|
|
44
|
+
|
|
45
|
+
### Function Mocks
|
|
46
|
+
\`\`\`typescript
|
|
47
|
+
// Basic mock
|
|
48
|
+
const mockFn = jest.fn();
|
|
49
|
+
mockFn.mockReturnValue('default');
|
|
50
|
+
mockFn.mockReturnValueOnce('first call');
|
|
51
|
+
|
|
52
|
+
// Async mock
|
|
53
|
+
const mockAsync = jest.fn();
|
|
54
|
+
mockAsync.mockResolvedValue({ data: 'success' });
|
|
55
|
+
mockAsync.mockRejectedValue(new Error('Failed'));
|
|
56
|
+
|
|
57
|
+
// Implementation mock
|
|
58
|
+
const mockImpl = jest.fn((x: number) => x * 2);
|
|
59
|
+
|
|
60
|
+
// Assertions
|
|
61
|
+
expect(mockFn).toHaveBeenCalled();
|
|
62
|
+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
|
|
63
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
64
|
+
expect(mockFn).toHaveBeenLastCalledWith('lastArg');
|
|
65
|
+
expect(mockFn).toHaveReturnedWith('value');
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
### Module Mocks
|
|
69
|
+
\`\`\`typescript
|
|
70
|
+
// Mock entire module
|
|
71
|
+
jest.mock('./userService');
|
|
72
|
+
import { UserService } from './userService';
|
|
73
|
+
const MockedUserService = UserService as jest.MockedClass<typeof UserService>;
|
|
74
|
+
|
|
75
|
+
// Mock with implementation
|
|
76
|
+
jest.mock('./api', () => ({
|
|
77
|
+
fetchUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test' }),
|
|
78
|
+
fetchPosts: jest.fn(),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Partial mock (keep some real implementations)
|
|
82
|
+
jest.mock('./utils', () => ({
|
|
83
|
+
...jest.requireActual('./utils'),
|
|
84
|
+
formatDate: jest.fn().mockReturnValue('2024-01-01'),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// Mock default export
|
|
88
|
+
jest.mock('./config', () => ({
|
|
89
|
+
__esModule: true,
|
|
90
|
+
default: { apiUrl: 'http://test.com' },
|
|
91
|
+
}));
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
### Spy on Methods
|
|
95
|
+
\`\`\`typescript
|
|
96
|
+
// Spy on object method
|
|
97
|
+
const spy = jest.spyOn(console, 'log');
|
|
98
|
+
spy.mockImplementation(() => {}); // Suppress output
|
|
99
|
+
|
|
100
|
+
// Spy on prototype
|
|
101
|
+
const saveSpy = jest.spyOn(UserService.prototype, 'save');
|
|
102
|
+
saveSpy.mockResolvedValue({ id: '1' });
|
|
103
|
+
|
|
104
|
+
// Restore original
|
|
105
|
+
spy.mockRestore();
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
## Async Testing
|
|
109
|
+
\`\`\`typescript
|
|
110
|
+
// Async/await
|
|
111
|
+
it('should fetch data', async () => {
|
|
112
|
+
const data = await fetchData();
|
|
113
|
+
expect(data).toEqual({ id: 1 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Promises
|
|
117
|
+
it('should resolve', () => {
|
|
118
|
+
return expect(fetchData()).resolves.toEqual({ id: 1 });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reject', () => {
|
|
122
|
+
return expect(fetchData()).rejects.toThrow('Error');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Callbacks (use done)
|
|
126
|
+
it('should callback', (done) => {
|
|
127
|
+
fetchWithCallback((err, data) => {
|
|
128
|
+
expect(err).toBeNull();
|
|
129
|
+
expect(data).toEqual({ id: 1 });
|
|
130
|
+
done();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Wait for assertions
|
|
135
|
+
it('should eventually update', async () => {
|
|
136
|
+
const result = await waitFor(() => {
|
|
137
|
+
expect(element).toHaveTextContent('Updated');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
## Timer Mocks
|
|
143
|
+
\`\`\`typescript
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
jest.useFakeTimers();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
jest.useRealTimers();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should debounce calls', () => {
|
|
153
|
+
const callback = jest.fn();
|
|
154
|
+
const debounced = debounce(callback, 1000);
|
|
155
|
+
|
|
156
|
+
debounced();
|
|
157
|
+
debounced();
|
|
158
|
+
debounced();
|
|
159
|
+
|
|
160
|
+
expect(callback).not.toHaveBeenCalled();
|
|
161
|
+
|
|
162
|
+
jest.advanceTimersByTime(1000);
|
|
163
|
+
|
|
164
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle setTimeout', () => {
|
|
168
|
+
const callback = jest.fn();
|
|
169
|
+
setTimeout(callback, 5000);
|
|
170
|
+
|
|
171
|
+
jest.runAllTimers(); // Run all pending timers
|
|
172
|
+
// Or: jest.advanceTimersByTime(5000);
|
|
173
|
+
|
|
174
|
+
expect(callback).toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// For Date mocking
|
|
178
|
+
jest.setSystemTime(new Date('2024-01-15'));
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
## Snapshot Testing
|
|
182
|
+
\`\`\`typescript
|
|
183
|
+
// Component snapshot
|
|
184
|
+
it('should match snapshot', () => {
|
|
185
|
+
const tree = renderer.create(<Button label="Click me" />).toJSON();
|
|
186
|
+
expect(tree).toMatchSnapshot();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Inline snapshot
|
|
190
|
+
it('should match inline snapshot', () => {
|
|
191
|
+
expect(formatUser(user)).toMatchInlineSnapshot(\`
|
|
192
|
+
{
|
|
193
|
+
"name": "John Doe",
|
|
194
|
+
"email": "john@example.com"
|
|
195
|
+
}
|
|
196
|
+
\`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Update snapshots: jest --updateSnapshot or jest -u
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
## Test Utilities
|
|
203
|
+
\`\`\`typescript
|
|
204
|
+
// Test each (parameterized tests)
|
|
205
|
+
describe.each([
|
|
206
|
+
{ input: 1, expected: 2 },
|
|
207
|
+
{ input: 2, expected: 4 },
|
|
208
|
+
{ input: 3, expected: 6 },
|
|
209
|
+
])('double($input)', ({ input, expected }) => {
|
|
210
|
+
it(\`should return \${expected}\`, () => {
|
|
211
|
+
expect(double(input)).toBe(expected);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Table syntax
|
|
216
|
+
it.each\`
|
|
217
|
+
a | b | expected
|
|
218
|
+
\${1} | \${2} | \${3}
|
|
219
|
+
\${2} | \${3} | \${5}
|
|
220
|
+
\`('add($a, $b) = $expected', ({ a, b, expected }) => {
|
|
221
|
+
expect(add(a, b)).toBe(expected);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Skip and focus
|
|
225
|
+
describe.skip('skipped suite', () => {});
|
|
226
|
+
it.skip('skipped test', () => {});
|
|
227
|
+
describe.only('focused suite', () => {}); // Only run this
|
|
228
|
+
it.only('focused test', () => {}); // Only run this
|
|
229
|
+
|
|
230
|
+
// Conditional tests
|
|
231
|
+
const itif = (condition: boolean) => condition ? it : it.skip;
|
|
232
|
+
itif(process.env.CI)('only runs in CI', () => {});
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
## Common Matchers
|
|
236
|
+
\`\`\`typescript
|
|
237
|
+
// Equality
|
|
238
|
+
expect(value).toBe(exact); // Strict equality (===)
|
|
239
|
+
expect(value).toEqual(object); // Deep equality
|
|
240
|
+
expect(value).toStrictEqual(object); // Deep equality + type checking
|
|
241
|
+
|
|
242
|
+
// Truthiness
|
|
243
|
+
expect(value).toBeTruthy();
|
|
244
|
+
expect(value).toBeFalsy();
|
|
245
|
+
expect(value).toBeNull();
|
|
246
|
+
expect(value).toBeUndefined();
|
|
247
|
+
expect(value).toBeDefined();
|
|
248
|
+
|
|
249
|
+
// Numbers
|
|
250
|
+
expect(value).toBeGreaterThan(3);
|
|
251
|
+
expect(value).toBeGreaterThanOrEqual(3);
|
|
252
|
+
expect(value).toBeLessThan(5);
|
|
253
|
+
expect(value).toBeCloseTo(0.3, 5); // Floating point
|
|
254
|
+
|
|
255
|
+
// Strings
|
|
256
|
+
expect(value).toMatch(/pattern/);
|
|
257
|
+
expect(value).toContain('substring');
|
|
258
|
+
|
|
259
|
+
// Arrays/Iterables
|
|
260
|
+
expect(array).toContain(item);
|
|
261
|
+
expect(array).toContainEqual({ id: 1 });
|
|
262
|
+
expect(array).toHaveLength(3);
|
|
263
|
+
|
|
264
|
+
// Objects
|
|
265
|
+
expect(object).toHaveProperty('key');
|
|
266
|
+
expect(object).toHaveProperty('nested.key', 'value');
|
|
267
|
+
expect(object).toMatchObject({ partial: 'match' });
|
|
268
|
+
|
|
269
|
+
// Errors
|
|
270
|
+
expect(() => fn()).toThrow();
|
|
271
|
+
expect(() => fn()).toThrow(Error);
|
|
272
|
+
expect(() => fn()).toThrow('message');
|
|
273
|
+
expect(() => fn()).toThrow(/pattern/);
|
|
274
|
+
|
|
275
|
+
// Negation
|
|
276
|
+
expect(value).not.toBe(other);
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
## Configuration (jest.config.js)
|
|
280
|
+
\`\`\`javascript
|
|
281
|
+
module.exports = {
|
|
282
|
+
preset: 'ts-jest',
|
|
283
|
+
testEnvironment: 'node', // or 'jsdom' for browser
|
|
284
|
+
roots: ['<rootDir>/src'],
|
|
285
|
+
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
|
286
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
|
287
|
+
coverageThreshold: {
|
|
288
|
+
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
|
|
289
|
+
},
|
|
290
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
291
|
+
moduleNameMapper: {
|
|
292
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
## ❌ DON'T
|
|
298
|
+
- Test implementation details
|
|
299
|
+
- Share state between tests
|
|
300
|
+
- Use \`.only\` in committed code
|
|
301
|
+
- Write flaky tests (timing-dependent)
|
|
302
|
+
- Mock everything (test real integrations too)
|
|
303
|
+
- Skip error case testing
|
|
304
|
+
|
|
305
|
+
## ✅ DO
|
|
306
|
+
- Follow Arrange-Act-Assert pattern
|
|
307
|
+
- Use descriptive test names
|
|
308
|
+
- Test behavior and outcomes
|
|
309
|
+
- Clear mocks between tests
|
|
310
|
+
- Test edge cases and errors
|
|
311
|
+
- Keep tests independent and isolated
|
|
312
|
+
- Use \`describe\` blocks to organize related tests
|
|
313
|
+
- Aim for meaningful coverage, not 100%
|