agentic-team-templates 0.13.1 → 0.14.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 +22 -2
- package/src/index.test.js +5 -0
- 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,92 @@
|
|
|
1
|
+
# C# Expert Overview
|
|
2
|
+
|
|
3
|
+
Principal-level C# engineering. Deep .NET runtime knowledge, modern language features, and production-grade patterns.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This guide applies to:
|
|
8
|
+
- Web APIs and services (ASP.NET Core, Minimal APIs)
|
|
9
|
+
- Desktop and cross-platform applications (WPF, MAUI, Avalonia)
|
|
10
|
+
- Cloud-native services (Azure Functions, Worker Services)
|
|
11
|
+
- Libraries and NuGet packages
|
|
12
|
+
- Real-time systems (SignalR, gRPC)
|
|
13
|
+
- Background processing (Hosted Services, message consumers)
|
|
14
|
+
|
|
15
|
+
## Core Philosophy
|
|
16
|
+
|
|
17
|
+
C# is a language of deliberate design. Every feature exists for a reason — use the right tool for the job.
|
|
18
|
+
|
|
19
|
+
- **Type safety is your first line of defense.** Nullable reference types enabled, warnings as errors.
|
|
20
|
+
- **Composition over inheritance.** Interfaces, extension methods, and dependency injection — not deep class hierarchies.
|
|
21
|
+
- **Async all the way down.** Never block on async code. Never use `.Result` or `.Wait()` in application code.
|
|
22
|
+
- **The framework does the heavy lifting.** ASP.NET Core's middleware pipeline, DI container, and configuration system are battle-tested — use them.
|
|
23
|
+
- **Measure before you optimize.** BenchmarkDotNet and dotnet-counters before rewriting anything.
|
|
24
|
+
- **If you don't know, say so.** Admitting uncertainty is professional. Guessing at runtime behavior you haven't verified is not.
|
|
25
|
+
|
|
26
|
+
## Key Principles
|
|
27
|
+
|
|
28
|
+
1. **Nullable Reference Types Are Non-Negotiable** — `<Nullable>enable</Nullable>` in every project
|
|
29
|
+
2. **Prefer Records for Data** — Immutable by default, value semantics, concise syntax
|
|
30
|
+
3. **Dependency Injection Is the Architecture** — Constructor injection, interface segregation, composition root
|
|
31
|
+
4. **Errors Are Explicit** — Result patterns for expected failures, exceptions for exceptional conditions
|
|
32
|
+
5. **Tests Describe Behavior** — Not implementation details
|
|
33
|
+
|
|
34
|
+
## Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Solution/
|
|
38
|
+
├── src/
|
|
39
|
+
│ ├── MyApp.Api/ # ASP.NET Core host (thin — wiring only)
|
|
40
|
+
│ │ ├── Program.cs # Composition root
|
|
41
|
+
│ │ ├── Endpoints/ # Minimal API endpoint definitions
|
|
42
|
+
│ │ └── Middleware/ # Custom middleware
|
|
43
|
+
│ ├── MyApp.Application/ # Use cases, commands, queries (no framework deps)
|
|
44
|
+
│ │ ├── Commands/
|
|
45
|
+
│ │ ├── Queries/
|
|
46
|
+
│ │ └── Interfaces/
|
|
47
|
+
│ ├── MyApp.Domain/ # Core domain (zero dependencies)
|
|
48
|
+
│ │ ├── Entities/
|
|
49
|
+
│ │ ├── ValueObjects/
|
|
50
|
+
│ │ └── Events/
|
|
51
|
+
│ └── MyApp.Infrastructure/ # External concerns (DB, HTTP, messaging)
|
|
52
|
+
│ ├── Persistence/
|
|
53
|
+
│ ├── Services/
|
|
54
|
+
│ └── Configuration/
|
|
55
|
+
├── tests/
|
|
56
|
+
│ ├── MyApp.UnitTests/
|
|
57
|
+
│ ├── MyApp.IntegrationTests/
|
|
58
|
+
│ └── MyApp.ArchitectureTests/
|
|
59
|
+
├── Directory.Build.props # Shared project settings
|
|
60
|
+
├── .editorconfig # Code style enforcement
|
|
61
|
+
└── MyApp.sln
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Directory.Build.props
|
|
65
|
+
|
|
66
|
+
```xml
|
|
67
|
+
<Project>
|
|
68
|
+
<PropertyGroup>
|
|
69
|
+
<TargetFramework>net9.0</TargetFramework>
|
|
70
|
+
<Nullable>enable</Nullable>
|
|
71
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
72
|
+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
73
|
+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
|
74
|
+
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
|
75
|
+
</PropertyGroup>
|
|
76
|
+
</Project>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Definition of Done
|
|
80
|
+
|
|
81
|
+
A C# feature is complete when:
|
|
82
|
+
|
|
83
|
+
- [ ] `dotnet build --warnaserror` passes with zero warnings
|
|
84
|
+
- [ ] `dotnet test` passes with no failures
|
|
85
|
+
- [ ] Nullable reference types produce no warnings
|
|
86
|
+
- [ ] No `#pragma warning disable` without inline justification
|
|
87
|
+
- [ ] Async methods don't block (no `.Result`, `.Wait()`, `.GetAwaiter().GetResult()`)
|
|
88
|
+
- [ ] All public APIs have XML documentation comments
|
|
89
|
+
- [ ] Error paths are tested
|
|
90
|
+
- [ ] DI registrations verified (no missing services at runtime)
|
|
91
|
+
- [ ] No `TODO` without an associated issue
|
|
92
|
+
- [ ] Code reviewed and approved
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# C# Performance
|
|
2
|
+
|
|
3
|
+
Measure first. Optimize second. BenchmarkDotNet is your truth.
|
|
4
|
+
|
|
5
|
+
## Profile Before Optimizing
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
// BenchmarkDotNet for micro-benchmarks
|
|
9
|
+
[MemoryDiagnoser]
|
|
10
|
+
[SimpleJob(RuntimeMoniker.Net90)]
|
|
11
|
+
public class StringConcatBenchmark
|
|
12
|
+
{
|
|
13
|
+
private readonly string[] _items = Enumerable.Range(0, 1000)
|
|
14
|
+
.Select(i => i.ToString()).ToArray();
|
|
15
|
+
|
|
16
|
+
[Benchmark(Baseline = true)]
|
|
17
|
+
public string Concatenation()
|
|
18
|
+
{
|
|
19
|
+
var result = "";
|
|
20
|
+
foreach (var item in _items)
|
|
21
|
+
result += item;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[Benchmark]
|
|
26
|
+
public string StringBuilder()
|
|
27
|
+
{
|
|
28
|
+
var sb = new StringBuilder();
|
|
29
|
+
foreach (var item in _items)
|
|
30
|
+
sb.Append(item);
|
|
31
|
+
return sb.ToString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[Benchmark]
|
|
35
|
+
public string StringJoin()
|
|
36
|
+
=> string.Join("", _items);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Diagnostic Tools
|
|
41
|
+
|
|
42
|
+
| Tool | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| `dotnet-counters` | Live runtime metrics (GC, thread pool, exceptions) |
|
|
45
|
+
| `dotnet-trace` | Performance tracing |
|
|
46
|
+
| `dotnet-dump` | Memory dump analysis |
|
|
47
|
+
| `dotnet-gcdump` | GC heap analysis |
|
|
48
|
+
| BenchmarkDotNet | Micro-benchmarks with statistical rigor |
|
|
49
|
+
|
|
50
|
+
## Allocation Patterns
|
|
51
|
+
|
|
52
|
+
### Reduce Allocations
|
|
53
|
+
|
|
54
|
+
```csharp
|
|
55
|
+
// Bad: allocates on every call
|
|
56
|
+
public string FormatName(string first, string last)
|
|
57
|
+
=> $"{first} {last}"; // Allocates interpolated string
|
|
58
|
+
|
|
59
|
+
// Good: use string.Create or Span for hot paths
|
|
60
|
+
public string FormatName(ReadOnlySpan<char> first, ReadOnlySpan<char> last)
|
|
61
|
+
=> string.Create(first.Length + 1 + last.Length, (first.ToString(), last.ToString()),
|
|
62
|
+
(span, state) =>
|
|
63
|
+
{
|
|
64
|
+
state.Item1.AsSpan().CopyTo(span);
|
|
65
|
+
span[state.Item1.Length] = ' ';
|
|
66
|
+
state.Item2.AsSpan().CopyTo(span[(state.Item1.Length + 1)..]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Better for most cases: just use StringBuilder
|
|
70
|
+
// Premature optimization with Span is worse than readable code
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### ArrayPool and MemoryPool
|
|
74
|
+
|
|
75
|
+
```csharp
|
|
76
|
+
// Rent buffers instead of allocating
|
|
77
|
+
public async Task<byte[]> CompressAsync(Stream input, CancellationToken ct)
|
|
78
|
+
{
|
|
79
|
+
var buffer = ArrayPool<byte>.Shared.Rent(8192);
|
|
80
|
+
try
|
|
81
|
+
{
|
|
82
|
+
using var output = new MemoryStream();
|
|
83
|
+
using var compressor = new GZipStream(output, CompressionLevel.Optimal);
|
|
84
|
+
|
|
85
|
+
int bytesRead;
|
|
86
|
+
while ((bytesRead = await input.ReadAsync(buffer, ct)) > 0)
|
|
87
|
+
{
|
|
88
|
+
await compressor.WriteAsync(buffer.AsMemory(0, bytesRead), ct);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await compressor.FlushAsync(ct);
|
|
92
|
+
return output.ToArray();
|
|
93
|
+
}
|
|
94
|
+
finally
|
|
95
|
+
{
|
|
96
|
+
ArrayPool<byte>.Shared.Return(buffer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Object Pooling
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
// ObjectPool for expensive-to-create objects
|
|
105
|
+
builder.Services.AddSingleton<ObjectPool<StringBuilder>>(
|
|
106
|
+
new DefaultObjectPoolProvider().CreateStringBuilderPool());
|
|
107
|
+
|
|
108
|
+
public class ReportGenerator(ObjectPool<StringBuilder> pool)
|
|
109
|
+
{
|
|
110
|
+
public string Generate(ReportData data)
|
|
111
|
+
{
|
|
112
|
+
var sb = pool.Get();
|
|
113
|
+
try
|
|
114
|
+
{
|
|
115
|
+
sb.AppendLine($"Report: {data.Title}");
|
|
116
|
+
foreach (var item in data.Items)
|
|
117
|
+
sb.AppendLine($"- {item.Name}: {item.Value}");
|
|
118
|
+
return sb.ToString();
|
|
119
|
+
}
|
|
120
|
+
finally
|
|
121
|
+
{
|
|
122
|
+
pool.Return(sb);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## struct vs class
|
|
129
|
+
|
|
130
|
+
```csharp
|
|
131
|
+
// Use struct when:
|
|
132
|
+
// - Logically represents a single value
|
|
133
|
+
// - Instance size < 16 bytes (guideline, not rule)
|
|
134
|
+
// - Immutable
|
|
135
|
+
// - Won't be boxed frequently
|
|
136
|
+
public readonly record struct Coordinate(double Lat, double Lon);
|
|
137
|
+
|
|
138
|
+
// Use class when:
|
|
139
|
+
// - Reference semantics needed
|
|
140
|
+
// - Will be used polymorphically
|
|
141
|
+
// - Large (copying structs is expensive)
|
|
142
|
+
// - Needs to be nullable (struct? has overhead)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Collection Performance
|
|
146
|
+
|
|
147
|
+
```csharp
|
|
148
|
+
// FrozenDictionary/FrozenSet for read-heavy, write-once scenarios (.NET 8+)
|
|
149
|
+
private static readonly FrozenDictionary<string, Handler> _handlers =
|
|
150
|
+
new Dictionary<string, Handler>
|
|
151
|
+
{
|
|
152
|
+
["GET"] = new GetHandler(),
|
|
153
|
+
["POST"] = new PostHandler(),
|
|
154
|
+
}.ToFrozenDictionary();
|
|
155
|
+
|
|
156
|
+
// Use CollectionsMarshal for high-performance dictionary access
|
|
157
|
+
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(
|
|
158
|
+
dictionary, key, out bool exists);
|
|
159
|
+
if (!exists) value = ComputeExpensiveValue(key);
|
|
160
|
+
|
|
161
|
+
// Correct collection type for the job
|
|
162
|
+
// List<T>: random access, append
|
|
163
|
+
// HashSet<T>: uniqueness, O(1) Contains
|
|
164
|
+
// Dictionary<K,V>: O(1) lookup by key
|
|
165
|
+
// Queue<T>/Stack<T>: FIFO/LIFO
|
|
166
|
+
// LinkedList<T>: frequent insert/remove in middle (rare in practice)
|
|
167
|
+
// SortedSet<T>: ordered unique elements
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## EF Core Performance
|
|
171
|
+
|
|
172
|
+
```csharp
|
|
173
|
+
// Always use AsNoTracking for read-only queries
|
|
174
|
+
var users = await _db.Users
|
|
175
|
+
.AsNoTracking()
|
|
176
|
+
.Where(u => u.IsActive)
|
|
177
|
+
.Select(u => new UserDto(u.Id, u.Name, u.Email)) // Project to DTO
|
|
178
|
+
.ToListAsync(ct);
|
|
179
|
+
|
|
180
|
+
// Avoid N+1 queries — use Include or projection
|
|
181
|
+
// Bad:
|
|
182
|
+
var orders = await _db.Orders.ToListAsync(ct);
|
|
183
|
+
foreach (var order in orders)
|
|
184
|
+
{
|
|
185
|
+
var items = order.Items; // Lazy load = N+1 queries!
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Good:
|
|
189
|
+
var orders = await _db.Orders
|
|
190
|
+
.Include(o => o.Items)
|
|
191
|
+
.ToListAsync(ct);
|
|
192
|
+
|
|
193
|
+
// Even better: project to exactly what you need
|
|
194
|
+
var orderSummaries = await _db.Orders
|
|
195
|
+
.Select(o => new OrderSummary(o.Id, o.Total, o.Items.Count))
|
|
196
|
+
.ToListAsync(ct);
|
|
197
|
+
|
|
198
|
+
// Use compiled queries for hot paths
|
|
199
|
+
private static readonly Func<AppDbContext, int, Task<User?>> GetUserById =
|
|
200
|
+
EF.CompileAsyncQuery((AppDbContext db, int id) =>
|
|
201
|
+
db.Users.FirstOrDefault(u => u.Id == id));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Caching
|
|
205
|
+
|
|
206
|
+
```csharp
|
|
207
|
+
// IMemoryCache for single-instance caching
|
|
208
|
+
public async Task<User?> GetUserAsync(int id, CancellationToken ct)
|
|
209
|
+
{
|
|
210
|
+
return await _cache.GetOrCreateAsync($"user:{id}", async entry =>
|
|
211
|
+
{
|
|
212
|
+
entry.SetSlidingExpiration(TimeSpan.FromMinutes(5));
|
|
213
|
+
entry.SetAbsoluteExpiration(TimeSpan.FromHours(1));
|
|
214
|
+
entry.SetSize(1); // For bounded caches
|
|
215
|
+
return await _repository.GetByIdAsync(id, ct);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// IDistributedCache for multi-instance (Redis, SQL Server)
|
|
220
|
+
// HybridCache (.NET 9+) for stampede protection
|
|
221
|
+
public async Task<User?> GetUserAsync(int id, CancellationToken ct)
|
|
222
|
+
{
|
|
223
|
+
return await _hybridCache.GetOrCreateAsync(
|
|
224
|
+
$"user:{id}",
|
|
225
|
+
async token => await _repository.GetByIdAsync(id, token),
|
|
226
|
+
cancellationToken: ct);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Anti-Patterns
|
|
231
|
+
|
|
232
|
+
```csharp
|
|
233
|
+
// Never: premature optimization without measurement
|
|
234
|
+
// "I think StringBuilder is faster" — prove it with a benchmark
|
|
235
|
+
|
|
236
|
+
// Never: LINQ in hot loops when a simple for loop suffices
|
|
237
|
+
for (int i = 0; i < items.Length; i++) // Fine for hot paths
|
|
238
|
+
Process(items[i]);
|
|
239
|
+
|
|
240
|
+
// Never: multiple enumeration of IEnumerable
|
|
241
|
+
var count = query.Count(); // Executes query
|
|
242
|
+
var list = query.ToList(); // Executes query AGAIN
|
|
243
|
+
// Materialize once: var list = query.ToList(); var count = list.Count;
|
|
244
|
+
|
|
245
|
+
// Never: blocking async in constructors
|
|
246
|
+
public MyService()
|
|
247
|
+
{
|
|
248
|
+
_data = LoadDataAsync().Result; // Deadlock risk
|
|
249
|
+
}
|
|
250
|
+
// Use async factory method or lazy initialization
|
|
251
|
+
```
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# C# Testing
|
|
2
|
+
|
|
3
|
+
Test behavior, not implementation. Every test should answer: "what does this code do?"
|
|
4
|
+
|
|
5
|
+
## Framework Stack
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| xUnit | Test framework (preferred for .NET) |
|
|
10
|
+
| NSubstitute | Mocking (clean syntax, less ceremony than Moq) |
|
|
11
|
+
| FluentAssertions | Readable assertions |
|
|
12
|
+
| Bogus | Test data generation |
|
|
13
|
+
| Testcontainers | Real databases/services in integration tests |
|
|
14
|
+
| Verify | Snapshot testing |
|
|
15
|
+
| ArchUnitNET | Architecture tests |
|
|
16
|
+
|
|
17
|
+
## Unit Test Structure
|
|
18
|
+
|
|
19
|
+
```csharp
|
|
20
|
+
public class OrderServiceTests
|
|
21
|
+
{
|
|
22
|
+
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
|
|
23
|
+
private readonly IInventoryService _inventory = Substitute.For<IInventoryService>();
|
|
24
|
+
private readonly OrderService _sut;
|
|
25
|
+
|
|
26
|
+
public OrderServiceTests()
|
|
27
|
+
{
|
|
28
|
+
_sut = new OrderService(_repository, _inventory);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[Fact]
|
|
32
|
+
public async Task CreateOrder_WithValidItems_ReturnsOrder()
|
|
33
|
+
{
|
|
34
|
+
// Arrange
|
|
35
|
+
var request = new CreateOrderRequest("customer-1", [new("sku-1", 2)]);
|
|
36
|
+
_inventory.CheckAvailabilityAsync("sku-1", 2, Arg.Any<CancellationToken>())
|
|
37
|
+
.Returns(true);
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
var result = await _sut.CreateAsync(request, CancellationToken.None);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
result.IsSuccess.Should().BeTrue();
|
|
44
|
+
result.Value!.CustomerId.Should().Be("customer-1");
|
|
45
|
+
result.Value.Items.Should().HaveCount(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[Fact]
|
|
49
|
+
public async Task CreateOrder_WithInsufficientInventory_ReturnsError()
|
|
50
|
+
{
|
|
51
|
+
// Arrange
|
|
52
|
+
var request = new CreateOrderRequest("customer-1", [new("sku-1", 100)]);
|
|
53
|
+
_inventory.CheckAvailabilityAsync("sku-1", 100, Arg.Any<CancellationToken>())
|
|
54
|
+
.Returns(false);
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
var result = await _sut.CreateAsync(request, CancellationToken.None);
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
result.IsSuccess.Should().BeFalse();
|
|
61
|
+
result.Error!.Code.Should().Be("INSUFFICIENT_INVENTORY");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Test Naming
|
|
67
|
+
|
|
68
|
+
```csharp
|
|
69
|
+
// Pattern: Method_Scenario_ExpectedBehavior
|
|
70
|
+
[Fact]
|
|
71
|
+
public async Task GetByEmail_WhenUserExists_ReturnsUser() { }
|
|
72
|
+
|
|
73
|
+
[Fact]
|
|
74
|
+
public async Task GetByEmail_WhenUserNotFound_ReturnsNull() { }
|
|
75
|
+
|
|
76
|
+
[Fact]
|
|
77
|
+
public async Task Register_WithDuplicateEmail_ReturnsConflictError() { }
|
|
78
|
+
|
|
79
|
+
// Or descriptive phrases
|
|
80
|
+
[Fact]
|
|
81
|
+
public async Task Newly_created_order_has_pending_status() { }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Theory (Parameterized Tests)
|
|
85
|
+
|
|
86
|
+
```csharp
|
|
87
|
+
[Theory]
|
|
88
|
+
[InlineData("", false)]
|
|
89
|
+
[InlineData("a", false)]
|
|
90
|
+
[InlineData("ab", false)]
|
|
91
|
+
[InlineData("abc", true)]
|
|
92
|
+
[InlineData("valid@email.com", true)]
|
|
93
|
+
public void Validate_Password_MinLength(string input, bool expectedValid)
|
|
94
|
+
{
|
|
95
|
+
var result = PasswordValidator.IsValid(input);
|
|
96
|
+
result.Should().Be(expectedValid);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Complex data with MemberData
|
|
100
|
+
[Theory]
|
|
101
|
+
[MemberData(nameof(InvalidOrderTestCases))]
|
|
102
|
+
public async Task CreateOrder_WithInvalidData_ReturnsValidationError(
|
|
103
|
+
CreateOrderRequest request, string expectedErrorCode)
|
|
104
|
+
{
|
|
105
|
+
var result = await _sut.CreateAsync(request, CancellationToken.None);
|
|
106
|
+
|
|
107
|
+
result.IsSuccess.Should().BeFalse();
|
|
108
|
+
result.Error!.Code.Should().Be(expectedErrorCode);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public static IEnumerable<object[]> InvalidOrderTestCases()
|
|
112
|
+
{
|
|
113
|
+
yield return [new CreateOrderRequest("", []), "VALIDATION"];
|
|
114
|
+
yield return [new CreateOrderRequest("c1", []), "VALIDATION"];
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Integration Tests with WebApplicationFactory
|
|
119
|
+
|
|
120
|
+
```csharp
|
|
121
|
+
public class OrderEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
|
122
|
+
{
|
|
123
|
+
private readonly HttpClient _client;
|
|
124
|
+
|
|
125
|
+
public OrderEndpointTests(WebApplicationFactory<Program> factory)
|
|
126
|
+
{
|
|
127
|
+
_client = factory.WithWebHostBuilder(builder =>
|
|
128
|
+
{
|
|
129
|
+
builder.ConfigureServices(services =>
|
|
130
|
+
{
|
|
131
|
+
// Replace real services with test doubles
|
|
132
|
+
services.RemoveAll<IOrderRepository>();
|
|
133
|
+
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
|
|
134
|
+
});
|
|
135
|
+
}).CreateClient();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[Fact]
|
|
139
|
+
public async Task POST_orders_returns_created_for_valid_request()
|
|
140
|
+
{
|
|
141
|
+
// Arrange
|
|
142
|
+
var request = new CreateOrderRequest("customer-1", [new("sku-1", 1)]);
|
|
143
|
+
|
|
144
|
+
// Act
|
|
145
|
+
var response = await _client.PostAsJsonAsync("/orders", request);
|
|
146
|
+
|
|
147
|
+
// Assert
|
|
148
|
+
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
149
|
+
var order = await response.Content.ReadFromJsonAsync<OrderResponse>();
|
|
150
|
+
order!.CustomerId.Should().Be("customer-1");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[Fact]
|
|
154
|
+
public async Task GET_orders_id_returns_not_found_for_missing_order()
|
|
155
|
+
{
|
|
156
|
+
var response = await _client.GetAsync("/orders/nonexistent");
|
|
157
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Integration Tests with Testcontainers
|
|
163
|
+
|
|
164
|
+
```csharp
|
|
165
|
+
public class PostgresOrderRepositoryTests : IAsyncLifetime
|
|
166
|
+
{
|
|
167
|
+
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
|
|
168
|
+
.WithImage("postgres:16-alpine")
|
|
169
|
+
.Build();
|
|
170
|
+
|
|
171
|
+
private AppDbContext _dbContext = null!;
|
|
172
|
+
|
|
173
|
+
public async Task InitializeAsync()
|
|
174
|
+
{
|
|
175
|
+
await _postgres.StartAsync();
|
|
176
|
+
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
177
|
+
.UseNpgsql(_postgres.GetConnectionString())
|
|
178
|
+
.Options;
|
|
179
|
+
_dbContext = new AppDbContext(options);
|
|
180
|
+
await _dbContext.Database.MigrateAsync();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async Task DisposeAsync()
|
|
184
|
+
{
|
|
185
|
+
await _dbContext.DisposeAsync();
|
|
186
|
+
await _postgres.DisposeAsync();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
[Fact]
|
|
190
|
+
public async Task SaveAndRetrieve_RoundTrips_Correctly()
|
|
191
|
+
{
|
|
192
|
+
var repo = new OrderRepository(_dbContext);
|
|
193
|
+
var order = Order.Create("customer-1", [new OrderItem("sku-1", 2, 9.99m)]);
|
|
194
|
+
|
|
195
|
+
await repo.SaveAsync(order, CancellationToken.None);
|
|
196
|
+
var retrieved = await repo.GetByIdAsync(order.Id, CancellationToken.None);
|
|
197
|
+
|
|
198
|
+
retrieved.Should().NotBeNull();
|
|
199
|
+
retrieved!.CustomerId.Should().Be("customer-1");
|
|
200
|
+
retrieved.Items.Should().HaveCount(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Architecture Tests
|
|
206
|
+
|
|
207
|
+
```csharp
|
|
208
|
+
public class ArchitectureTests
|
|
209
|
+
{
|
|
210
|
+
private static readonly Architecture Architecture =
|
|
211
|
+
new ArchLoader().LoadAssemblies(
|
|
212
|
+
typeof(Order).Assembly, // Domain
|
|
213
|
+
typeof(OrderService).Assembly, // Application
|
|
214
|
+
typeof(OrderRepository).Assembly // Infrastructure
|
|
215
|
+
).Build();
|
|
216
|
+
|
|
217
|
+
[Fact]
|
|
218
|
+
public void Domain_should_not_depend_on_infrastructure()
|
|
219
|
+
{
|
|
220
|
+
Types().That().ResideInNamespace("MyApp.Domain")
|
|
221
|
+
.Should().NotDependOnAny("MyApp.Infrastructure")
|
|
222
|
+
.Check(Architecture);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[Fact]
|
|
226
|
+
public void Application_should_not_depend_on_aspnetcore()
|
|
227
|
+
{
|
|
228
|
+
Types().That().ResideInNamespace("MyApp.Application")
|
|
229
|
+
.Should().NotDependOnAny("Microsoft.AspNetCore")
|
|
230
|
+
.Check(Architecture);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Test Data Builders
|
|
236
|
+
|
|
237
|
+
```csharp
|
|
238
|
+
// Bogus for realistic fake data
|
|
239
|
+
public static class TestData
|
|
240
|
+
{
|
|
241
|
+
private static readonly Faker<CreateOrderRequest> OrderFaker = new Faker<CreateOrderRequest>()
|
|
242
|
+
.CustomInstantiator(f => new CreateOrderRequest(
|
|
243
|
+
CustomerId: f.Random.Guid().ToString(),
|
|
244
|
+
Items: f.Make(f.Random.Int(1, 5), () =>
|
|
245
|
+
new OrderItemRequest(f.Commerce.Ean13(), f.Random.Int(1, 10)))));
|
|
246
|
+
|
|
247
|
+
public static CreateOrderRequest ValidOrder() => OrderFaker.Generate();
|
|
248
|
+
|
|
249
|
+
// Builder pattern for specific scenarios
|
|
250
|
+
public static CreateOrderRequest OrderWithItems(int count) =>
|
|
251
|
+
OrderFaker.Generate() with
|
|
252
|
+
{
|
|
253
|
+
Items = Enumerable.Range(0, count)
|
|
254
|
+
.Select(i => new OrderItemRequest($"sku-{i}", 1))
|
|
255
|
+
.ToList()
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Anti-Patterns
|
|
261
|
+
|
|
262
|
+
```csharp
|
|
263
|
+
// Never: testing implementation details
|
|
264
|
+
_repository.Received(1).SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
|
|
265
|
+
// Test the outcome instead: verify the order was actually persisted
|
|
266
|
+
|
|
267
|
+
// Never: shared mutable state between tests
|
|
268
|
+
private static List<Order> _orders = []; // Tests pollute each other
|
|
269
|
+
// Use fresh state in constructor or Setup
|
|
270
|
+
|
|
271
|
+
// Never: Thread.Sleep in tests
|
|
272
|
+
await Task.Delay(5000); // Flaky and slow
|
|
273
|
+
// Use polling with timeout, or Polly retry, or proper async synchronization
|
|
274
|
+
|
|
275
|
+
// Never: testing trivial code
|
|
276
|
+
[Fact]
|
|
277
|
+
public void Name_getter_returns_name() // Waste of time
|
|
278
|
+
{
|
|
279
|
+
var user = new User { Name = "Alice" };
|
|
280
|
+
user.Name.Should().Be("Alice");
|
|
281
|
+
}
|
|
282
|
+
```
|