@coralai/sps-cli 0.42.0 → 0.43.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 +34 -3
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Java — Concurrency
|
|
2
|
+
|
|
3
|
+
Virtual threads (21+), `ExecutorService`, `CompletableFuture`, structured concurrency.
|
|
4
|
+
|
|
5
|
+
## Virtual threads — Java 21+
|
|
6
|
+
|
|
7
|
+
Cheap threads mapped many-to-one onto OS threads. For I/O-bound work, replace platform-thread pools.
|
|
8
|
+
|
|
9
|
+
```java
|
|
10
|
+
// One virtual thread per request — simple, no thread-pool sizing
|
|
11
|
+
var executor = Executors.newVirtualThreadPerTaskExecutor();
|
|
12
|
+
executor.submit(() -> handle(req));
|
|
13
|
+
|
|
14
|
+
// Or inline:
|
|
15
|
+
Thread.startVirtualThread(() -> handle(req));
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Virtual threads block cheaply. A thread per request becomes a reasonable model. `Thread.sleep`, blocking I/O (JDBC, filesystem), locks — all yield the carrier thread rather than burning it.
|
|
19
|
+
|
|
20
|
+
When NOT to use virtual threads:
|
|
21
|
+
- CPU-bound work (thousands of virtual threads running compute just migrate between OS threads; no gain).
|
|
22
|
+
- Code heavy on `synchronized` blocks (pinning problem — before Java 24 the carrier is locked; better in 24+).
|
|
23
|
+
- Legacy reactive code where migration is a separate project.
|
|
24
|
+
|
|
25
|
+
## Platform threads — `ExecutorService`
|
|
26
|
+
|
|
27
|
+
For CPU-bound work or when you need explicit thread-pool sizing.
|
|
28
|
+
|
|
29
|
+
```java
|
|
30
|
+
try (var exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {
|
|
31
|
+
var futures = items.stream()
|
|
32
|
+
.map(i -> exec.submit(() -> compute(i)))
|
|
33
|
+
.toList();
|
|
34
|
+
for (var f : futures) results.add(f.get());
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`try-with-resources` on `ExecutorService` (Java 19+) shuts it down cleanly.
|
|
39
|
+
|
|
40
|
+
Sizing:
|
|
41
|
+
- CPU-bound: `n = cores` or `cores + 1`.
|
|
42
|
+
- Mixed: `n = cores × (1 + wait/compute ratio)`.
|
|
43
|
+
- If you can't estimate, virtual threads take the question away.
|
|
44
|
+
|
|
45
|
+
## `CompletableFuture` — composable async
|
|
46
|
+
|
|
47
|
+
```java
|
|
48
|
+
CompletableFuture<User> userF = fetchUser(id);
|
|
49
|
+
CompletableFuture<Prefs> prefsF = fetchPrefs(id);
|
|
50
|
+
|
|
51
|
+
CompletableFuture<Screen> result = userF.thenCombine(prefsF, Screen::new);
|
|
52
|
+
|
|
53
|
+
result.thenAccept(screen -> render(screen))
|
|
54
|
+
.exceptionally(err -> { log.error(err); return null; });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`supplyAsync`, `thenApply`, `thenCompose`, `thenCombine`, `allOf`, `anyOf`, `exceptionally` — the basic toolkit.
|
|
58
|
+
|
|
59
|
+
**Always** provide an explicit `Executor`:
|
|
60
|
+
|
|
61
|
+
```java
|
|
62
|
+
CompletableFuture.supplyAsync(this::load, executor)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Default is `ForkJoinPool.commonPool()` — OK for CPU-bound, wrong for I/O. Named executors are clearer and tune-able.
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
- `.get()` blocks; use `.join()` in lambdas if you must.
|
|
69
|
+
- Don't mix async chains with blocking calls; stay reactive all the way or don't bother.
|
|
70
|
+
- With virtual threads, plain imperative code is usually preferable to `CompletableFuture` chains.
|
|
71
|
+
|
|
72
|
+
## Structured concurrency (preview → stable)
|
|
73
|
+
|
|
74
|
+
```java
|
|
75
|
+
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
|
|
76
|
+
Subtask<User> userT = scope.fork(() -> fetchUser(id));
|
|
77
|
+
Subtask<Prefs> prefsT = scope.fork(() -> fetchPrefs(id));
|
|
78
|
+
|
|
79
|
+
scope.join().throwIfFailed();
|
|
80
|
+
return new Screen(userT.get(), prefsT.get());
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Ties child tasks' lifecycle to the enclosing scope. Compiler / runtime ensures all children finish (or are cancelled) before the scope exits. Pairs nicely with virtual threads.
|
|
85
|
+
|
|
86
|
+
Check your JDK version — this API has been in preview; use the stable namespace (available in 25 LTS+).
|
|
87
|
+
|
|
88
|
+
## Locks
|
|
89
|
+
|
|
90
|
+
| Use | Tool |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Short critical section | `synchronized` block |
|
|
93
|
+
| Read-heavy | `ReadWriteLock` / `StampedLock` |
|
|
94
|
+
| Atomic counter / flag | `java.util.concurrent.atomic.*` |
|
|
95
|
+
| One-time init | `volatile` + double-checked init, or `Holder` class pattern |
|
|
96
|
+
|
|
97
|
+
```java
|
|
98
|
+
private final Object lock = new Object();
|
|
99
|
+
private int counter;
|
|
100
|
+
|
|
101
|
+
void inc() {
|
|
102
|
+
synchronized (lock) { counter++; }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Prefer `AtomicInteger` / `AtomicReference` over mutex + primitive for simple cases.
|
|
107
|
+
|
|
108
|
+
`Lock` interface (`ReentrantLock`) for tryLock, timeouts, fairness. Don't reach for it unless you need one of those.
|
|
109
|
+
|
|
110
|
+
## Concurrent collections
|
|
111
|
+
|
|
112
|
+
| Type | Use |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `ConcurrentHashMap<K,V>` | Thread-safe map; scales to many cores |
|
|
115
|
+
| `CopyOnWriteArrayList<T>` | Mostly read, rarely written; snapshots |
|
|
116
|
+
| `BlockingQueue<T>` (`LinkedBlockingQueue`, etc.) | Producer / consumer |
|
|
117
|
+
| `ConcurrentLinkedQueue<T>` | Unbounded lock-free queue |
|
|
118
|
+
|
|
119
|
+
Don't wrap a regular `HashMap` in `Collections.synchronizedMap` and call it concurrent — it serializes every operation.
|
|
120
|
+
|
|
121
|
+
## `volatile`
|
|
122
|
+
|
|
123
|
+
Publishes writes to other threads. Does NOT make compound operations atomic.
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
private volatile boolean running = true;
|
|
127
|
+
// other thread can see running = false without extra sync
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Don't use `volatile` for counters. Use `AtomicInteger` / `AtomicLong` — `i++` is not atomic on `volatile int`.
|
|
131
|
+
|
|
132
|
+
## `ThreadLocal`
|
|
133
|
+
|
|
134
|
+
Per-thread storage. With platform threads + thread pools, a `ThreadLocal` can leak across requests because threads are reused. Use `try` / `finally` to clean up, or switch to `ScopedValue` (Java 21+).
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
|
|
138
|
+
|
|
139
|
+
ScopedValue.where(CURRENT_USER, user).run(() -> {
|
|
140
|
+
// CURRENT_USER.get() is user for the duration of this block
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`ScopedValue` is immutable, bound by lexical scope, plays well with virtual threads.
|
|
145
|
+
|
|
146
|
+
## Avoid blocking in async frameworks
|
|
147
|
+
|
|
148
|
+
If you're on Reactor / RxJava / Mutiny (Quarkus), a blocking call on an event loop stalls the whole world. Mark blocking operations for a different scheduler:
|
|
149
|
+
|
|
150
|
+
```java
|
|
151
|
+
Mono.fromCallable(this::blockingThing).subscribeOn(Schedulers.boundedElastic());
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or move to virtual threads and stop needing reactive at all.
|
|
155
|
+
|
|
156
|
+
## Shutdown hooks
|
|
157
|
+
|
|
158
|
+
```java
|
|
159
|
+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
160
|
+
exec.shutdown();
|
|
161
|
+
try { exec.awaitTermination(30, TimeUnit.SECONDS); } catch (Exception ignored) {}
|
|
162
|
+
db.close();
|
|
163
|
+
}));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Web frameworks do most of this for you. In plain-Java apps, do it explicitly.
|
|
167
|
+
|
|
168
|
+
## Timeouts
|
|
169
|
+
|
|
170
|
+
Every network / DB call has a timeout. Defaults are usually "infinite" or "very long".
|
|
171
|
+
|
|
172
|
+
```java
|
|
173
|
+
// HttpClient
|
|
174
|
+
var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
|
|
175
|
+
var req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(5)).GET().build();
|
|
176
|
+
|
|
177
|
+
// JDBC
|
|
178
|
+
statement.setQueryTimeout(5);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
See `backend/references/resilience.md` for the full budget model.
|
|
182
|
+
|
|
183
|
+
## Anti-patterns
|
|
184
|
+
|
|
185
|
+
| Anti-pattern | Fix |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `Thread.sleep` in request handlers | Use scheduled executors; or `VirtualThread.sleep` if truly needed |
|
|
188
|
+
| `new Thread(...).start()` for each task | Use an `ExecutorService` |
|
|
189
|
+
| `synchronized` across I/O calls | Hold locks briefly; never across network |
|
|
190
|
+
| Catching `InterruptedException` and discarding | `Thread.currentThread().interrupt()` before returning, or rethrow |
|
|
191
|
+
| `CompletableFuture` chains without an explicit executor | Always specify |
|
|
192
|
+
| `.get()` without timeout | `.get(timeout, unit)` or restructure async |
|
|
193
|
+
| Mutable shared state without synchronization | Lock it, use concurrent collections, or use an actor-ish pattern |
|
|
194
|
+
| `Future.cancel(true)` assumed to stop blocking I/O | Cancel interrupts; blocking calls may not honour it |
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Java — Idioms
|
|
2
|
+
|
|
3
|
+
Records, sealed types, streams, `Optional`, modern collections.
|
|
4
|
+
|
|
5
|
+
## Records — data classes done right
|
|
6
|
+
|
|
7
|
+
```java
|
|
8
|
+
public record User(String id, String email, boolean active) {}
|
|
9
|
+
|
|
10
|
+
var u = new User("u1", "a@x.com", true);
|
|
11
|
+
u.email(); // accessor
|
|
12
|
+
new User("u1", "a@x.com", false).equals(u); // false — structural equality
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Records give you `equals`, `hashCode`, `toString`, and component accessors for free.
|
|
16
|
+
|
|
17
|
+
Compact canonical constructor for validation:
|
|
18
|
+
|
|
19
|
+
```java
|
|
20
|
+
public record Email(String value) {
|
|
21
|
+
public Email {
|
|
22
|
+
if (!value.contains("@")) throw new IllegalArgumentException("bad email");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Don't use records when you need mutation, inheritance, or behaviour beyond data + validation.
|
|
28
|
+
|
|
29
|
+
## Sealed types + pattern switch
|
|
30
|
+
|
|
31
|
+
Model sum types exhaustively.
|
|
32
|
+
|
|
33
|
+
```java
|
|
34
|
+
public sealed interface Result<T>
|
|
35
|
+
permits Result.Ok, Result.Err {
|
|
36
|
+
record Ok<T>(T value) implements Result<T> {}
|
|
37
|
+
record Err<T>(String error) implements Result<T> {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
String handle(Result<User> r) {
|
|
41
|
+
return switch (r) {
|
|
42
|
+
case Result.Ok(var u) -> "got " + u.email();
|
|
43
|
+
case Result.Err(var msg) -> "fail " + msg;
|
|
44
|
+
}; // no default — compiler enforces exhaustiveness
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`permits` limits implementers to the named classes. Add a new variant → every `switch` breaks until updated.
|
|
49
|
+
|
|
50
|
+
## `var` — locals only
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
var users = userRepo.findAll(); // List<User>
|
|
54
|
+
var map = new HashMap<String, List<Order>>();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Don't use `var` in method signatures, field declarations, or where the inferred type would surprise a reader.
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
// ❌ — what is x?
|
|
61
|
+
var x = svc.doThing();
|
|
62
|
+
|
|
63
|
+
// ✅ keep the type when not obvious
|
|
64
|
+
List<User> users = svc.loadUsers();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## `Optional<T>`
|
|
68
|
+
|
|
69
|
+
For **return values** that might legitimately be absent.
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
Optional<User> findById(String id) { ... }
|
|
73
|
+
|
|
74
|
+
findById(id)
|
|
75
|
+
.map(User::email)
|
|
76
|
+
.orElseThrow(() -> new NotFoundException("no user " + id));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Never:
|
|
80
|
+
- `Optional` as a field type (serialization headache, mutable wrapper)
|
|
81
|
+
- `Optional` as a method parameter (overloads are clearer)
|
|
82
|
+
- `Optional.get()` without `isPresent()` (same bug as `unwrap`)
|
|
83
|
+
|
|
84
|
+
## Collections
|
|
85
|
+
|
|
86
|
+
```java
|
|
87
|
+
List<String> immutable = List.of("a", "b", "c");
|
|
88
|
+
Map<String, Integer> m = Map.of("a", 1, "b", 2);
|
|
89
|
+
Set<String> s = Set.of("x", "y");
|
|
90
|
+
|
|
91
|
+
var mutable = new ArrayList<String>();
|
|
92
|
+
mutable.add("a");
|
|
93
|
+
|
|
94
|
+
var copy = List.copyOf(mutable); // defensive copy, unmodifiable
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Prefer `List.of` / `Map.of` for small known data. Prefer `ArrayList` / `HashMap` for mutable.
|
|
98
|
+
|
|
99
|
+
## Streams
|
|
100
|
+
|
|
101
|
+
Concise collection transformations.
|
|
102
|
+
|
|
103
|
+
```java
|
|
104
|
+
var emails = users.stream()
|
|
105
|
+
.filter(User::active)
|
|
106
|
+
.map(User::email)
|
|
107
|
+
.toList();
|
|
108
|
+
|
|
109
|
+
var byRole = users.stream()
|
|
110
|
+
.collect(Collectors.groupingBy(User::role));
|
|
111
|
+
|
|
112
|
+
var count = users.stream().filter(User::active).count();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Rules:
|
|
116
|
+
- `toList()` (Java 16+) over `collect(Collectors.toList())`.
|
|
117
|
+
- Don't use streams for simple for-loops with side effects — the loop is clearer.
|
|
118
|
+
- Don't mix stream and `forEach` for state mutation; prefer `collect` / `reduce`.
|
|
119
|
+
|
|
120
|
+
Stream of a single op doesn't improve readability:
|
|
121
|
+
|
|
122
|
+
```java
|
|
123
|
+
// ❌
|
|
124
|
+
list.stream().filter(p).findFirst().orElse(null);
|
|
125
|
+
|
|
126
|
+
// ✅
|
|
127
|
+
list.stream().filter(p).findFirst().orElseThrow();
|
|
128
|
+
// or use a library method if it exists
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## `switch` expressions
|
|
132
|
+
|
|
133
|
+
```java
|
|
134
|
+
String label = switch (status) {
|
|
135
|
+
case ACTIVE -> "active";
|
|
136
|
+
case PENDING -> "pending";
|
|
137
|
+
case BANNED -> "banned";
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Pattern matching (preview/stable depending on JDK)
|
|
141
|
+
var description = switch (shape) {
|
|
142
|
+
case Circle c -> "circle r=" + c.radius();
|
|
143
|
+
case Square s -> "square s=" + s.side();
|
|
144
|
+
case null, default -> "unknown";
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Arrow `->` form has no fall-through. Traditional `case X:` has fall-through — avoid.
|
|
149
|
+
|
|
150
|
+
## Exceptions
|
|
151
|
+
|
|
152
|
+
- **Checked** (`extends Exception`) — recoverable, documented in signature.
|
|
153
|
+
- **Unchecked** (`extends RuntimeException`) — programmer errors, boundary validation.
|
|
154
|
+
|
|
155
|
+
```java
|
|
156
|
+
public class ValidationException extends RuntimeException {
|
|
157
|
+
private final String field;
|
|
158
|
+
public ValidationException(String field, String msg) {
|
|
159
|
+
super(msg);
|
|
160
|
+
this.field = field;
|
|
161
|
+
}
|
|
162
|
+
public String field() { return field; }
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Chain the cause:
|
|
167
|
+
|
|
168
|
+
```java
|
|
169
|
+
try { ... }
|
|
170
|
+
catch (IOException e) {
|
|
171
|
+
throw new ConfigException("loading " + path, e); // ← pass e as cause
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Never catch `Throwable` / `Exception` and swallow. Catch specifics, log, handle or rethrow.
|
|
176
|
+
|
|
177
|
+
## `final` — for invariants
|
|
178
|
+
|
|
179
|
+
- On class: no subclasses.
|
|
180
|
+
- On method: can't override.
|
|
181
|
+
- On field: immutable reference.
|
|
182
|
+
- On local / param: can't reassign (noisy; `var` encourages it implicitly).
|
|
183
|
+
|
|
184
|
+
Records' components are implicitly final.
|
|
185
|
+
|
|
186
|
+
Don't sprinkle `final` on every local — IDEs and modern reviewers treat it as clutter unless the team convention says otherwise.
|
|
187
|
+
|
|
188
|
+
## Nullability
|
|
189
|
+
|
|
190
|
+
Java doesn't have built-in non-null types. Options:
|
|
191
|
+
|
|
192
|
+
- **`@Nullable` / `@NonNull`** from JSR-305, JetBrains, or Checker Framework.
|
|
193
|
+
- **Records + `@NotNull` in canonical constructor validation.**
|
|
194
|
+
- **Kotlin interop** — declare nullability to help Kotlin callers.
|
|
195
|
+
|
|
196
|
+
Pick one annotation set; configure your IDE + CI to enforce it.
|
|
197
|
+
|
|
198
|
+
## `java.time.*` — not `Date`
|
|
199
|
+
|
|
200
|
+
```java
|
|
201
|
+
var now = Instant.now();
|
|
202
|
+
var today = LocalDate.now();
|
|
203
|
+
var inZone = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
|
|
204
|
+
var later = now.plus(Duration.ofMinutes(30));
|
|
205
|
+
|
|
206
|
+
Instant.parse("2026-04-21T10:00:00Z");
|
|
207
|
+
DateTimeFormatter.ofPattern("yyyy-MM-dd").format(today);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`java.util.Date` and `Calendar` are deprecated in spirit; every use is a code smell in new code.
|
|
211
|
+
|
|
212
|
+
## Equality
|
|
213
|
+
|
|
214
|
+
- `equals()` for value equality; `==` for reference equality.
|
|
215
|
+
- `record` gets `equals` automatically.
|
|
216
|
+
- For classes, use IDE / Lombok / `EqualsBuilder` — never hand-write `equals` without `hashCode` to match.
|
|
217
|
+
|
|
218
|
+
```java
|
|
219
|
+
Objects.equals(a, b); // null-safe
|
|
220
|
+
Objects.hash(field1, field2);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## String
|
|
224
|
+
|
|
225
|
+
```java
|
|
226
|
+
String s = "hi";
|
|
227
|
+
String formatted = "user %s (%d)".formatted(name, count); // Java 15+
|
|
228
|
+
String multiline = """
|
|
229
|
+
Hello
|
|
230
|
+
%s
|
|
231
|
+
""".formatted(name); // text blocks (15+)
|
|
232
|
+
|
|
233
|
+
var sb = new StringBuilder(); // for building in loops
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`String.format` for static strings is fine. `"...".formatted(...)` reads better.
|
|
237
|
+
|
|
238
|
+
`String.repeat(n)`, `String.strip()` (vs `trim()` which is ASCII-only) are modern.
|
|
239
|
+
|
|
240
|
+
## Lambdas vs. method references
|
|
241
|
+
|
|
242
|
+
```java
|
|
243
|
+
users.stream().map(u -> u.email()); // lambda
|
|
244
|
+
users.stream().map(User::email); // method ref — preferred when trivial
|
|
245
|
+
|
|
246
|
+
users.stream().filter(u -> u.active());
|
|
247
|
+
users.stream().filter(User::active);
|
|
248
|
+
|
|
249
|
+
// Constructor reference
|
|
250
|
+
users.stream().map(name -> new User(name)).toList();
|
|
251
|
+
users.stream().map(User::new).toList();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Method references when the operation is a direct member access. Lambdas when there's any extra logic.
|
|
255
|
+
|
|
256
|
+
## Builder pattern
|
|
257
|
+
|
|
258
|
+
For types with many optional fields, use a builder. Lombok provides `@Builder`; libraries like Immutables generate it.
|
|
259
|
+
|
|
260
|
+
```java
|
|
261
|
+
var req = HttpRequest.builder()
|
|
262
|
+
.url("https://x.com")
|
|
263
|
+
.timeout(Duration.ofSeconds(5))
|
|
264
|
+
.header("X-Auth", token)
|
|
265
|
+
.build();
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
For records, a static factory + named params via builder is common.
|
|
269
|
+
|
|
270
|
+
## Anti-patterns
|
|
271
|
+
|
|
272
|
+
| Anti-pattern | Fix |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `public` mutable fields | Getters or records |
|
|
275
|
+
| `if (x == null) throw new NullPointerException(...)` everywhere | Non-null annotations + validate at boundary |
|
|
276
|
+
| `instanceof x + cast` chains | Pattern switch / sealed types |
|
|
277
|
+
| Anonymous inner class for a SAM | Lambda |
|
|
278
|
+
| `Collections.unmodifiableList(new ArrayList<>(...))` | `List.copyOf(...)` |
|
|
279
|
+
| Hand-rolled `equals` / `hashCode` on data classes | Record or generated code |
|
|
280
|
+
| Catching `Exception e` to "simplify" | Catch specific; let unknown propagate |
|
|
281
|
+
| `System.out.println` in production | Use SLF4J / `java.util.logging` |
|
|
282
|
+
| `Thread.currentThread().sleep(...)` | `Thread.sleep(...)` — static method; use as such |
|
|
283
|
+
| `new Integer(x)` / `new String(x)` | Use valueOf / literal |
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Java — Testing
|
|
2
|
+
|
|
3
|
+
JUnit 5, AssertJ, Mockito, Testcontainers. For TDD, see `coding-standards/references/tdd.md`.
|
|
4
|
+
|
|
5
|
+
## JUnit 5 — the default
|
|
6
|
+
|
|
7
|
+
```java
|
|
8
|
+
import org.junit.jupiter.api.*;
|
|
9
|
+
import static org.junit.jupiter.api.Assertions.*;
|
|
10
|
+
|
|
11
|
+
class UserServiceTest {
|
|
12
|
+
private UserService service;
|
|
13
|
+
|
|
14
|
+
@BeforeEach
|
|
15
|
+
void setUp() { service = new UserService(new InMemoryUserRepo()); }
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
@DisplayName("creates a user with a generated id")
|
|
19
|
+
void create_returnsPersistedUser() {
|
|
20
|
+
var u = service.create("A", "a@x.com");
|
|
21
|
+
assertEquals("A", u.name());
|
|
22
|
+
assertNotNull(u.id());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
void create_rejectsEmptyEmail() {
|
|
27
|
+
assertThrows(ValidationException.class, () -> service.create("A", ""));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use `@DisplayName` for behaviour-focused names that don't fit Java method-name rules.
|
|
33
|
+
|
|
34
|
+
## AssertJ — fluent assertions
|
|
35
|
+
|
|
36
|
+
```java
|
|
37
|
+
import static org.assertj.core.api.Assertions.*;
|
|
38
|
+
|
|
39
|
+
assertThat(users)
|
|
40
|
+
.hasSize(3)
|
|
41
|
+
.extracting(User::email)
|
|
42
|
+
.contains("a@x.com")
|
|
43
|
+
.doesNotContain("banned@x.com");
|
|
44
|
+
|
|
45
|
+
assertThat(user.role()).isEqualTo(Role.ADMIN);
|
|
46
|
+
|
|
47
|
+
assertThatThrownBy(() -> service.create("A", ""))
|
|
48
|
+
.isInstanceOf(ValidationException.class)
|
|
49
|
+
.hasMessageContaining("email");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Much richer than JUnit's built-ins. Most Java codebases use AssertJ or Hamcrest. Pick one per project.
|
|
53
|
+
|
|
54
|
+
## Mockito — when you need mocks
|
|
55
|
+
|
|
56
|
+
Fakes > mocks (see `coding-standards` / `typescript`/`python` refs). But Mockito works when a fake is too heavy.
|
|
57
|
+
|
|
58
|
+
```java
|
|
59
|
+
import static org.mockito.Mockito.*;
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
void loadsFromRepo() {
|
|
63
|
+
var repo = mock(UserRepository.class);
|
|
64
|
+
when(repo.find("u1")).thenReturn(Optional.of(new User("u1", "a@x.com")));
|
|
65
|
+
|
|
66
|
+
var service = new UserService(repo);
|
|
67
|
+
assertEquals("a@x.com", service.getEmail("u1"));
|
|
68
|
+
|
|
69
|
+
verify(repo, times(1)).find("u1");
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`@Mock` + `@InjectMocks` for less boilerplate; enable with `@ExtendWith(MockitoExtension.class)`.
|
|
74
|
+
|
|
75
|
+
Don't mock types you own — change them instead. Mock boundaries (HTTP clients, clocks, external APIs).
|
|
76
|
+
|
|
77
|
+
## Parametrized tests
|
|
78
|
+
|
|
79
|
+
```java
|
|
80
|
+
@ParameterizedTest
|
|
81
|
+
@CsvSource({
|
|
82
|
+
"1, 2, 3",
|
|
83
|
+
"0, 0, 0",
|
|
84
|
+
"-1, 1, 0",
|
|
85
|
+
})
|
|
86
|
+
void add(int a, int b, int expected) {
|
|
87
|
+
assertEquals(expected, Math.addExact(a, b));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@ParameterizedTest
|
|
91
|
+
@EnumSource(Role.class)
|
|
92
|
+
void allRolesHaveLabel(Role r) {
|
|
93
|
+
assertNotNull(r.label());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@ParameterizedTest
|
|
97
|
+
@MethodSource("loginCases")
|
|
98
|
+
void login(LoginCase c) { ... }
|
|
99
|
+
static Stream<LoginCase> loginCases() { ... }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Lifecycle annotations
|
|
103
|
+
|
|
104
|
+
| Annotation | Runs |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `@BeforeEach` | Before each `@Test` |
|
|
107
|
+
| `@AfterEach` | After each `@Test` |
|
|
108
|
+
| `@BeforeAll` | Once before any test (static) |
|
|
109
|
+
| `@AfterAll` | Once after all tests (static) |
|
|
110
|
+
| `@Nested` class | Group related tests, fresh fixtures |
|
|
111
|
+
|
|
112
|
+
`@TestInstance(Lifecycle.PER_CLASS)` if you want non-static `@BeforeAll` or shared state.
|
|
113
|
+
|
|
114
|
+
## `@Nested` for readability
|
|
115
|
+
|
|
116
|
+
```java
|
|
117
|
+
@Nested
|
|
118
|
+
@DisplayName("when user is banned")
|
|
119
|
+
class WhenBanned {
|
|
120
|
+
@BeforeEach void setUp() { user = userWithStatus(BANNED); }
|
|
121
|
+
|
|
122
|
+
@Test void cannotLogin() { ... }
|
|
123
|
+
@Test void cannotPost() { ... }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Reads as a spec. Tests stay organized without helper functions exploding the file.
|
|
128
|
+
|
|
129
|
+
## Integration tests — Testcontainers
|
|
130
|
+
|
|
131
|
+
Real dependencies, Docker-driven, ephemeral.
|
|
132
|
+
|
|
133
|
+
```java
|
|
134
|
+
@Testcontainers
|
|
135
|
+
class UserRepoIT {
|
|
136
|
+
@Container
|
|
137
|
+
static final PostgreSQLContainer<?> PG =
|
|
138
|
+
new PostgreSQLContainer<>("postgres:16-alpine");
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
void savesAndLoads() throws Exception {
|
|
142
|
+
var ds = dataSource(PG.getJdbcUrl(), PG.getUsername(), PG.getPassword());
|
|
143
|
+
var repo = new JdbcUserRepository(ds);
|
|
144
|
+
repo.save(new User("u1", "a@x.com"));
|
|
145
|
+
assertThat(repo.find("u1")).isPresent();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Split integration tests (`*IT.java`) from unit tests (`*Test.java`) in the build so you don't run them every time.
|
|
151
|
+
|
|
152
|
+
## Spring Boot tests
|
|
153
|
+
|
|
154
|
+
Layered:
|
|
155
|
+
|
|
156
|
+
```java
|
|
157
|
+
@WebMvcTest(UserController.class) // only web layer + @MockBean repo
|
|
158
|
+
class UserControllerTest { ... }
|
|
159
|
+
|
|
160
|
+
@DataJpaTest // only JPA + embedded DB
|
|
161
|
+
class UserRepoTest { ... }
|
|
162
|
+
|
|
163
|
+
@SpringBootTest // full app context
|
|
164
|
+
class UserIntegrationTest { ... }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Prefer `@WebMvcTest` / `@DataJpaTest` over `@SpringBootTest` — they load a slice, are much faster.
|
|
168
|
+
|
|
169
|
+
`@TestContainers` + `@SpringBootTest` for real-DB integration tests on the production stack.
|
|
170
|
+
|
|
171
|
+
## Async tests
|
|
172
|
+
|
|
173
|
+
```java
|
|
174
|
+
@Test
|
|
175
|
+
void futureCompletes() throws Exception {
|
|
176
|
+
var f = asyncService.load(id);
|
|
177
|
+
assertEquals("a@x.com", f.get(5, TimeUnit.SECONDS).email());
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
For streams/flows, use Awaitility:
|
|
182
|
+
|
|
183
|
+
```java
|
|
184
|
+
import static org.awaitility.Awaitility.await;
|
|
185
|
+
|
|
186
|
+
await().atMost(5, SECONDS).until(() -> queue.size() > 0);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Property tests — jqwik
|
|
190
|
+
|
|
191
|
+
```java
|
|
192
|
+
@Property
|
|
193
|
+
boolean sortIsIdempotent(@ForAll List<@IntRange(min = -1000, max = 1000) Integer> xs) {
|
|
194
|
+
var sorted = xs.stream().sorted().toList();
|
|
195
|
+
var twice = sorted.stream().sorted().toList();
|
|
196
|
+
return sorted.equals(twice);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Niche but valuable for parsers, invariants.
|
|
201
|
+
|
|
202
|
+
## Coverage
|
|
203
|
+
|
|
204
|
+
JaCoCo is the default. Set a threshold in Maven / Gradle and fail CI on drop.
|
|
205
|
+
|
|
206
|
+
```xml
|
|
207
|
+
<!-- Maven: jacoco-maven-plugin -->
|
|
208
|
+
<rule>
|
|
209
|
+
<element>BUNDLE</element>
|
|
210
|
+
<limits>
|
|
211
|
+
<limit><counter>LINE</counter><minimum>0.80</minimum></limit>
|
|
212
|
+
</limits>
|
|
213
|
+
</rule>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Anti-patterns
|
|
217
|
+
|
|
218
|
+
| Anti-pattern | Fix |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `Thread.sleep` to wait for async | Awaitility / explicit futures |
|
|
221
|
+
| `@Test(expected = ...)` (JUnit 4) | JUnit 5: `assertThrows` |
|
|
222
|
+
| Mocking what you don't own without wrapping | Create a wrapper type; mock the wrapper |
|
|
223
|
+
| Tests that load full Spring context for a pure-logic test | `@WebMvcTest` / plain JUnit |
|
|
224
|
+
| Mockito `any()` for every arg | Assert specific args — catches bugs |
|
|
225
|
+
| Mocking final classes without Mockito inline extension | Enable `mockito-inline` or refactor to an interface |
|
|
226
|
+
| Shared mutable static state across tests | `@BeforeEach` reset or move state into instance |
|
|
227
|
+
| Snapshot / text asserts for time-dependent output | Inject a clock |
|
|
228
|
+
| Tests named `test1`, `test2` | Describe behaviour — `create_rejectsEmptyEmail` |
|