@grimoire-cc/cli 0.13.3 → 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/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +14 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/enforce.d.ts +3 -1
- package/dist/enforce.d.ts.map +1 -1
- package/dist/enforce.js +18 -6
- package/dist/enforce.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +47 -0
- package/dist/setup.js.map +1 -1
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +9 -0
- package/dist/summary.js.map +1 -1
- package/package.json +1 -1
- package/packs/dev-pack/agents/grimoire.tdd-specialist.md +194 -27
- package/packs/dev-pack/grimoire.json +0 -38
- package/packs/dev-pack/skills/grimoire.conventional-commit/SKILL.md +69 -65
- package/packs/dotnet-pack/agents/grimoire.csharp-coder.md +110 -113
- package/packs/dotnet-pack/grimoire.json +23 -5
- package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/SKILL.md +252 -0
- package/packs/{dev-pack/skills/grimoire.tdd-specialist → dotnet-pack/skills/grimoire.unit-testing-dotnet}/reference/anti-patterns.md +78 -0
- package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/reference/tdd-workflow-patterns.md +259 -0
- package/packs/go-pack/grimoire.json +19 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/SKILL.md +256 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/reference/anti-patterns.md +244 -0
- package/packs/go-pack/skills/grimoire.unit-testing-go/reference/tdd-workflow-patterns.md +259 -0
- package/packs/python-pack/grimoire.json +19 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/SKILL.md +239 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/reference/anti-patterns.md +244 -0
- package/packs/python-pack/skills/grimoire.unit-testing-python/reference/tdd-workflow-patterns.md +259 -0
- package/packs/rust-pack/grimoire.json +29 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/SKILL.md +243 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/anti-patterns.md +244 -0
- package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/tdd-workflow-patterns.md +259 -0
- package/packs/ts-pack/agents/grimoire.typescript-coder.md +36 -1
- package/packs/ts-pack/grimoire.json +27 -1
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/SKILL.md +255 -0
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/anti-patterns.md +244 -0
- package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/tdd-workflow-patterns.md +259 -0
- package/packs/dev-pack/skills/grimoire.tdd-specialist/SKILL.md +0 -248
- package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/language-frameworks.md +0 -388
- package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/tdd-workflow-patterns.md +0 -135
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/SKILL.md +0 -293
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/anti-patterns.md +0 -329
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/framework-guidelines.md +0 -361
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/parameterized-testing.md +0 -378
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-organization.md +0 -476
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-performance.md +0 -576
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/tunit-template.md +0 -438
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/xunit-template.md +0 -303
|
@@ -1,576 +0,0 @@
|
|
|
1
|
-
# Test Performance
|
|
2
|
-
|
|
3
|
-
Guidelines for optimizing test suite performance.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Why Performance Matters](#why-performance-matters)
|
|
8
|
-
- [Test Isolation for Parallel Execution](#test-isolation-for-parallel-execution)
|
|
9
|
-
- [Fixture Optimization](#fixture-optimization)
|
|
10
|
-
- [Async Test Performance](#async-test-performance)
|
|
11
|
-
- [Mock Performance](#mock-performance)
|
|
12
|
-
- [Database Test Performance](#database-test-performance)
|
|
13
|
-
- [Test Data Builders](#test-data-builders)
|
|
14
|
-
- [Measuring Performance](#measuring-performance)
|
|
15
|
-
- [Parallel Execution Decision Guide](#parallel-execution-decision-guide)
|
|
16
|
-
- [Performance Anti-Patterns](#performance-anti-patterns)
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Why Performance Matters
|
|
21
|
-
|
|
22
|
-
| Slow Tests Cause | Impact |
|
|
23
|
-
| ------------------ | -------- |
|
|
24
|
-
| Longer CI/CD pipelines | Delayed deployments, frustrated developers |
|
|
25
|
-
| Developers skip running tests locally | Bugs caught later, more expensive fixes |
|
|
26
|
-
| Test suite abandonment | Technical debt accumulates |
|
|
27
|
-
| Flaky tests (timeouts) | False negatives erode trust |
|
|
28
|
-
|
|
29
|
-
**Target benchmarks:**
|
|
30
|
-
|
|
31
|
-
- Unit tests: <100ms each, <10 seconds total for a module
|
|
32
|
-
- Integration tests: <1 second each where possible
|
|
33
|
-
- Full test suite: <5 minutes for CI feedback
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Test Isolation for Parallel Execution
|
|
38
|
-
|
|
39
|
-
Tests that are properly isolated can run in parallel safely, dramatically reducing execution time.
|
|
40
|
-
|
|
41
|
-
**Safe for parallel execution:**
|
|
42
|
-
|
|
43
|
-
```csharp
|
|
44
|
-
// Each test creates its own instance
|
|
45
|
-
public class OrderServiceTests
|
|
46
|
-
{
|
|
47
|
-
[Fact]
|
|
48
|
-
public async Task Test1()
|
|
49
|
-
{
|
|
50
|
-
var sut = new OrderService(new Mock<IRepo>().Object); // Own instance
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
[Fact]
|
|
54
|
-
public async Task Test2()
|
|
55
|
-
{
|
|
56
|
-
var sut = new OrderService(new Mock<IRepo>().Object); // Own instance
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**Will fail randomly in parallel:**
|
|
62
|
-
|
|
63
|
-
```csharp
|
|
64
|
-
// BAD: Shared mutable state
|
|
65
|
-
public class OrderServiceTests
|
|
66
|
-
{
|
|
67
|
-
private static List<Order> _orders = new(); // SHARED!
|
|
68
|
-
private static int _counter = 0; // SHARED!
|
|
69
|
-
|
|
70
|
-
[Fact]
|
|
71
|
-
public void Test1()
|
|
72
|
-
{
|
|
73
|
-
_orders.Add(new Order()); // Affects Test2!
|
|
74
|
-
_counter++;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
[Fact]
|
|
78
|
-
public void Test2()
|
|
79
|
-
{
|
|
80
|
-
Assert.Empty(_orders); // May fail if Test1 runs first!
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**What can be safely shared:**
|
|
86
|
-
|
|
87
|
-
| Safe to Share | Not Safe to Share |
|
|
88
|
-
| --------------- | ------------------- |
|
|
89
|
-
| Immutable data | Mutable collections |
|
|
90
|
-
| Configuration values | Counters, flags |
|
|
91
|
-
| Read-only test fixtures | Objects with state |
|
|
92
|
-
| Static helper methods | Static fields with state |
|
|
93
|
-
| Compiled regex patterns | Database connections (usually) |
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Fixture Optimization
|
|
98
|
-
|
|
99
|
-
### Class Fixture vs Collection Fixture
|
|
100
|
-
|
|
101
|
-
```csharp
|
|
102
|
-
// CLASS FIXTURE: Created once per test CLASS
|
|
103
|
-
public class OrderServiceTests : IClassFixture<DatabaseFixture>
|
|
104
|
-
{
|
|
105
|
-
private readonly DatabaseFixture _fixture;
|
|
106
|
-
|
|
107
|
-
public OrderServiceTests(DatabaseFixture fixture)
|
|
108
|
-
{
|
|
109
|
-
_fixture = fixture; // Same instance for all tests in this class
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// COLLECTION FIXTURE: Created once per test COLLECTION (multiple classes)
|
|
114
|
-
[Collection("Database")]
|
|
115
|
-
public class OrderRepositoryTests { }
|
|
116
|
-
|
|
117
|
-
[Collection("Database")]
|
|
118
|
-
public class CustomerRepositoryTests { }
|
|
119
|
-
// Both classes share the SAME DatabaseFixture instance
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
| Approach | Fixture Created | Best For |
|
|
123
|
-
| ---------- | ----------------- | ---------- |
|
|
124
|
-
| No fixture (constructor) | Once per TEST | Fast, isolated unit tests |
|
|
125
|
-
| `IClassFixture<T>` | Once per CLASS | Moderate setup, single class |
|
|
126
|
-
| `ICollectionFixture<T>` | Once per COLLECTION | Expensive setup, multiple classes |
|
|
127
|
-
|
|
128
|
-
### Lazy Initialization Pattern
|
|
129
|
-
|
|
130
|
-
```csharp
|
|
131
|
-
public class ExpensiveFixture : IAsyncLifetime
|
|
132
|
-
{
|
|
133
|
-
private TestDatabase? _database;
|
|
134
|
-
private HttpClient? _httpClient;
|
|
135
|
-
|
|
136
|
-
public TestDatabase Database => _database
|
|
137
|
-
?? throw new InvalidOperationException("Call InitializeDatabaseAsync first");
|
|
138
|
-
|
|
139
|
-
public Task InitializeAsync() => Task.CompletedTask; // Nothing eager
|
|
140
|
-
|
|
141
|
-
public async Task InitializeDatabaseAsync()
|
|
142
|
-
{
|
|
143
|
-
_database ??= await TestDatabase.CreateAsync();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
public async Task DisposeAsync()
|
|
147
|
-
{
|
|
148
|
-
if (_database != null) await _database.DisposeAsync();
|
|
149
|
-
_httpClient?.Dispose();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Tests only initialize what they need
|
|
154
|
-
public class OrderTests : IClassFixture<ExpensiveFixture>
|
|
155
|
-
{
|
|
156
|
-
private readonly ExpensiveFixture _fixture;
|
|
157
|
-
|
|
158
|
-
public OrderTests(ExpensiveFixture fixture) => _fixture = fixture;
|
|
159
|
-
|
|
160
|
-
[Fact]
|
|
161
|
-
public async Task DatabaseTest()
|
|
162
|
-
{
|
|
163
|
-
await _fixture.InitializeDatabaseAsync(); // Only this test pays the cost
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
[Fact]
|
|
167
|
-
public void FastUnitTest()
|
|
168
|
-
{
|
|
169
|
-
// Doesn't need database - doesn't pay initialization cost
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Async Test Performance
|
|
177
|
-
|
|
178
|
-
**Correct:**
|
|
179
|
-
|
|
180
|
-
```csharp
|
|
181
|
-
// Proper async all the way down
|
|
182
|
-
[Fact]
|
|
183
|
-
public async Task ProcessOrder_Async_Correct()
|
|
184
|
-
{
|
|
185
|
-
var result = await _sut.ProcessOrderAsync(order);
|
|
186
|
-
Assert.True(result.IsSuccess);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Parallel async operations when independent
|
|
190
|
-
[Fact]
|
|
191
|
-
public async Task MultipleOperations_RunInParallel()
|
|
192
|
-
{
|
|
193
|
-
var task1 = _sut.GetOrderAsync(id1);
|
|
194
|
-
var task2 = _sut.GetOrderAsync(id2);
|
|
195
|
-
var task3 = _sut.GetOrderAsync(id3);
|
|
196
|
-
|
|
197
|
-
var results = await Task.WhenAll(task1, task2, task3);
|
|
198
|
-
|
|
199
|
-
Assert.All(results, r => Assert.NotNull(r));
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Avoid:**
|
|
204
|
-
|
|
205
|
-
```csharp
|
|
206
|
-
// BAD: Blocking on async - can deadlock
|
|
207
|
-
[Fact]
|
|
208
|
-
public void ProcessOrder_Blocking_Wrong()
|
|
209
|
-
{
|
|
210
|
-
var result = _sut.ProcessOrderAsync(order).Result; // BLOCKS!
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// BAD: Unnecessary Task.Run - adds overhead
|
|
214
|
-
[Fact]
|
|
215
|
-
public async Task ProcessOrder_UnnecessaryTaskRun()
|
|
216
|
-
{
|
|
217
|
-
var result = await Task.Run(() => _sut.ProcessOrderAsync(order)); // Unnecessary!
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Async Timeout Patterns
|
|
222
|
-
|
|
223
|
-
**xUnit:**
|
|
224
|
-
|
|
225
|
-
```csharp
|
|
226
|
-
[Fact]
|
|
227
|
-
public async Task LongRunningOperation_CompletesWithinTimeout()
|
|
228
|
-
{
|
|
229
|
-
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
230
|
-
var result = await _sut.ProcessAsync(cts.Token);
|
|
231
|
-
Assert.True(result.IsSuccess);
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**TUnit:**
|
|
236
|
-
|
|
237
|
-
```csharp
|
|
238
|
-
[Test]
|
|
239
|
-
[Timeout(5000)] // 5 seconds
|
|
240
|
-
public async Task LongRunningOperation_CompletesWithinTimeout()
|
|
241
|
-
{
|
|
242
|
-
var result = await _sut.ProcessAsync();
|
|
243
|
-
await Assert.That(result.IsSuccess).IsTrue();
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Mock Performance
|
|
250
|
-
|
|
251
|
-
### Strict vs Loose Mocks
|
|
252
|
-
|
|
253
|
-
```csharp
|
|
254
|
-
// LOOSE MOCK (default) - Better performance, less brittle
|
|
255
|
-
var mock = new Mock<IRepository>();
|
|
256
|
-
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(entity);
|
|
257
|
-
// Unsetup methods return default values - no verification overhead
|
|
258
|
-
|
|
259
|
-
// STRICT MOCK - Slower, more brittle, use sparingly
|
|
260
|
-
var strictMock = new Mock<IRepository>(MockBehavior.Strict);
|
|
261
|
-
// EVERY call must be setup - throws on unexpected calls
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Efficient Mock Setup
|
|
265
|
-
|
|
266
|
-
```csharp
|
|
267
|
-
// SLOW: Creating new mock for every test case
|
|
268
|
-
[Theory]
|
|
269
|
-
[InlineData(1)]
|
|
270
|
-
[InlineData(2)]
|
|
271
|
-
public async Task Test(int id)
|
|
272
|
-
{
|
|
273
|
-
var mock = new Mock<IRepository>(); // Created multiple times!
|
|
274
|
-
mock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync(new Entity { Id = id });
|
|
275
|
-
var sut = new Service(mock.Object);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// FASTER: Reuse mock, configure per-test
|
|
279
|
-
public class ServiceTests
|
|
280
|
-
{
|
|
281
|
-
private readonly Mock<IRepository> _mock = new();
|
|
282
|
-
private readonly Service _sut;
|
|
283
|
-
|
|
284
|
-
public ServiceTests()
|
|
285
|
-
{
|
|
286
|
-
_sut = new Service(_mock.Object);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
[Theory]
|
|
290
|
-
[InlineData(1)]
|
|
291
|
-
[InlineData(2)]
|
|
292
|
-
public async Task Test(int id)
|
|
293
|
-
{
|
|
294
|
-
_mock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync(new Entity { Id = id });
|
|
295
|
-
_mock.Reset(); // Clean up for next test if needed
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Avoid Over-Verification
|
|
301
|
-
|
|
302
|
-
```csharp
|
|
303
|
-
// SLOW & BRITTLE: Verifying everything
|
|
304
|
-
[Fact]
|
|
305
|
-
public async Task ProcessOrder_OverVerified()
|
|
306
|
-
{
|
|
307
|
-
await _sut.ProcessOrderAsync(order);
|
|
308
|
-
|
|
309
|
-
_mockRepo.Verify(r => r.BeginTransactionAsync(), Times.Once);
|
|
310
|
-
_mockRepo.Verify(r => r.GetByIdAsync(It.IsAny<Guid>()), Times.Once);
|
|
311
|
-
_mockRepo.Verify(r => r.UpdateAsync(It.IsAny<Order>()), Times.Once);
|
|
312
|
-
_mockRepo.Verify(r => r.SaveChangesAsync(), Times.Once);
|
|
313
|
-
// 4+ verification calls - slow and tests implementation
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// FAST & FOCUSED: Verify only what matters
|
|
317
|
-
[Fact]
|
|
318
|
-
public async Task ProcessOrder_Focused()
|
|
319
|
-
{
|
|
320
|
-
await _sut.ProcessOrderAsync(order);
|
|
321
|
-
|
|
322
|
-
_mockRepo.Verify(r => r.UpdateAsync(
|
|
323
|
-
It.Is<Order>(o => o.Status == OrderStatus.Processed)),
|
|
324
|
-
Times.Once);
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## Database Test Performance
|
|
331
|
-
|
|
332
|
-
### Use Transactions for Isolation and Speed
|
|
333
|
-
|
|
334
|
-
```csharp
|
|
335
|
-
public class DatabaseTests : IAsyncLifetime
|
|
336
|
-
{
|
|
337
|
-
private IDbContextTransaction _transaction = null!;
|
|
338
|
-
private AppDbContext _context = null!;
|
|
339
|
-
|
|
340
|
-
public async Task InitializeAsync()
|
|
341
|
-
{
|
|
342
|
-
_context = _db.CreateContext();
|
|
343
|
-
_transaction = await _context.Database.BeginTransactionAsync();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
public async Task DisposeAsync()
|
|
347
|
-
{
|
|
348
|
-
await _transaction.RollbackAsync(); // Fast cleanup - no data persisted
|
|
349
|
-
await _transaction.DisposeAsync();
|
|
350
|
-
await _context.DisposeAsync();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
[Fact]
|
|
354
|
-
public async Task CreateOrder_PersistsToDatabase()
|
|
355
|
-
{
|
|
356
|
-
var order = new Order { /* ... */ };
|
|
357
|
-
_context.Orders.Add(order);
|
|
358
|
-
await _context.SaveChangesAsync();
|
|
359
|
-
|
|
360
|
-
var saved = await _context.Orders.FindAsync(order.Id);
|
|
361
|
-
Assert.NotNull(saved);
|
|
362
|
-
// Transaction rolls back - database unchanged for next test
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### In-Memory Database for Unit Tests
|
|
368
|
-
|
|
369
|
-
```csharp
|
|
370
|
-
public class OrderRepositoryTests
|
|
371
|
-
{
|
|
372
|
-
private readonly AppDbContext _context;
|
|
373
|
-
|
|
374
|
-
public OrderRepositoryTests()
|
|
375
|
-
{
|
|
376
|
-
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
377
|
-
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) // Unique per test
|
|
378
|
-
.Options;
|
|
379
|
-
|
|
380
|
-
_context = new AppDbContext(options);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
[Fact]
|
|
384
|
-
public async Task GetById_ReturnsOrder()
|
|
385
|
-
{
|
|
386
|
-
// In-memory - very fast, but not 100% SQL compatible
|
|
387
|
-
_context.Orders.Add(new Order { Id = 1 });
|
|
388
|
-
await _context.SaveChangesAsync();
|
|
389
|
-
|
|
390
|
-
var result = await _context.Orders.FindAsync(1);
|
|
391
|
-
Assert.NotNull(result);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## Test Data Builders
|
|
399
|
-
|
|
400
|
-
### Builder Pattern
|
|
401
|
-
|
|
402
|
-
```csharp
|
|
403
|
-
public class OrderBuilder
|
|
404
|
-
{
|
|
405
|
-
private int _id = 1;
|
|
406
|
-
private string _orderNumber = "ORD-001";
|
|
407
|
-
private OrderStatus _status = OrderStatus.Pending;
|
|
408
|
-
private List<OrderItem> _items = new();
|
|
409
|
-
private Customer? _customer;
|
|
410
|
-
|
|
411
|
-
public OrderBuilder WithId(int id) { _id = id; return this; }
|
|
412
|
-
public OrderBuilder WithStatus(OrderStatus status) { _status = status; return this; }
|
|
413
|
-
public OrderBuilder WithItem(OrderItem item) { _items.Add(item); return this; }
|
|
414
|
-
|
|
415
|
-
public Order Build() => new Order
|
|
416
|
-
{
|
|
417
|
-
Id = _id,
|
|
418
|
-
OrderNumber = _orderNumber,
|
|
419
|
-
Status = _status,
|
|
420
|
-
Items = _items,
|
|
421
|
-
Customer = _customer ?? new Customer { Name = "Default" }
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Usage - only build what the test needs
|
|
426
|
-
[Fact]
|
|
427
|
-
public void Test_OnlyNeedsStatus()
|
|
428
|
-
{
|
|
429
|
-
var order = new OrderBuilder()
|
|
430
|
-
.WithStatus(OrderStatus.Shipped)
|
|
431
|
-
.Build();
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Object Mother Pattern
|
|
436
|
-
|
|
437
|
-
```csharp
|
|
438
|
-
public static class TestOrders
|
|
439
|
-
{
|
|
440
|
-
public static Order ValidPendingOrder => new Order
|
|
441
|
-
{
|
|
442
|
-
Id = 1,
|
|
443
|
-
Status = OrderStatus.Pending,
|
|
444
|
-
Items = { new OrderItem { ProductId = 1, Quantity = 1 } }
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
public static Order EmptyOrder => new Order { Id = 2 };
|
|
448
|
-
|
|
449
|
-
public static Order CreateWithItems(int itemCount) => new Order
|
|
450
|
-
{
|
|
451
|
-
Items = Enumerable.Range(1, itemCount)
|
|
452
|
-
.Select(i => new OrderItem { ProductId = i, Quantity = 1 })
|
|
453
|
-
.ToList()
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
## Measuring Performance
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
# Run tests with timing information
|
|
464
|
-
dotnet test --logger "console;verbosity=detailed"
|
|
465
|
-
|
|
466
|
-
# Output test results to TRX file for analysis
|
|
467
|
-
dotnet test --logger "trx;LogFileName=results.trx"
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
**xUnit - Filter slow tests:**
|
|
471
|
-
|
|
472
|
-
```csharp
|
|
473
|
-
[Fact]
|
|
474
|
-
[Trait("Speed", "Slow")]
|
|
475
|
-
public async Task SlowIntegrationTest() { }
|
|
476
|
-
|
|
477
|
-
// CI fast feedback: dotnet test --filter "Speed!=Slow"
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**TUnit - Built-in timing:**
|
|
481
|
-
|
|
482
|
-
```csharp
|
|
483
|
-
[Test]
|
|
484
|
-
[Timeout(1000)] // Fail if >1 second
|
|
485
|
-
public async Task ShouldBeFast() { }
|
|
486
|
-
|
|
487
|
-
[Test]
|
|
488
|
-
[Retry(3)] // Retry flaky tests
|
|
489
|
-
public async Task OccasionallySlowTest() { }
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Parallel Execution Decision Guide
|
|
495
|
-
|
|
496
|
-
| Scenario | Parallel? | Reason |
|
|
497
|
-
| ---------- | ----------- | -------- |
|
|
498
|
-
| Pure unit tests (no I/O) | Yes | No shared state, fast |
|
|
499
|
-
| Tests with mocked dependencies | Yes | Mocks are isolated |
|
|
500
|
-
| In-memory database tests | Yes | Each gets unique DB name |
|
|
501
|
-
| Real database tests | Depends | Need transaction isolation |
|
|
502
|
-
| File system tests | Depends | Use unique temp directories |
|
|
503
|
-
| Tests modifying static state | No | Will interfere with each other |
|
|
504
|
-
| Tests using shared external service | No | Rate limits, state pollution |
|
|
505
|
-
| Tests with specific port requirements | No | Port conflicts |
|
|
506
|
-
|
|
507
|
-
### Configuring Parallel Execution
|
|
508
|
-
|
|
509
|
-
```csharp
|
|
510
|
-
// xUnit: xunit.runner.json
|
|
511
|
-
{
|
|
512
|
-
"parallelizeAssembly": true,
|
|
513
|
-
"parallelizeTestCollections": true,
|
|
514
|
-
"maxParallelThreads": 0 // 0 = use all processors
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// xUnit: Limit parallelism for resource-intensive tests
|
|
518
|
-
[assembly: CollectionBehavior(MaxParallelThreads = 4)]
|
|
519
|
-
|
|
520
|
-
// TUnit: Configure via attribute
|
|
521
|
-
[assembly: Parallelism(MaxConcurrency = 4)]
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
---
|
|
525
|
-
|
|
526
|
-
## Performance Anti-Patterns
|
|
527
|
-
|
|
528
|
-
```csharp
|
|
529
|
-
// ANTI-PATTERN: Thread.Sleep in tests
|
|
530
|
-
[Fact]
|
|
531
|
-
public async Task WaitForEvent_Wrong()
|
|
532
|
-
{
|
|
533
|
-
_sut.TriggerEvent();
|
|
534
|
-
Thread.Sleep(1000); // Wastes 1 second!
|
|
535
|
-
Assert.True(_sut.EventProcessed);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// CORRECT: Use async wait with timeout
|
|
539
|
-
[Fact]
|
|
540
|
-
public async Task WaitForEvent_Correct()
|
|
541
|
-
{
|
|
542
|
-
_sut.TriggerEvent();
|
|
543
|
-
await WaitForConditionAsync(() => _sut.EventProcessed, timeout: TimeSpan.FromSeconds(5));
|
|
544
|
-
Assert.True(_sut.EventProcessed);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ANTI-PATTERN: Creating database per test
|
|
548
|
-
[Fact]
|
|
549
|
-
public async Task Test1()
|
|
550
|
-
{
|
|
551
|
-
var db = await TestDatabase.CreateAsync(); // 500ms+ per test!
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// CORRECT: Share database via fixture
|
|
555
|
-
[Collection("Database")]
|
|
556
|
-
public class Tests
|
|
557
|
-
{
|
|
558
|
-
public Tests(DatabaseFixture fixture) { } // Created once for collection
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// ANTI-PATTERN: Large test data for every test
|
|
562
|
-
[Fact]
|
|
563
|
-
public void ValidateOrder_ChecksName()
|
|
564
|
-
{
|
|
565
|
-
var order = CreateFullOrderWith100Items(); // Only need Name!
|
|
566
|
-
Assert.False(_sut.Validate(order with { Name = "" }));
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// CORRECT: Minimal test data
|
|
570
|
-
[Fact]
|
|
571
|
-
public void ValidateOrder_ChecksName()
|
|
572
|
-
{
|
|
573
|
-
var order = new Order { Name = "" }; // Only what's needed
|
|
574
|
-
Assert.False(_sut.Validate(order));
|
|
575
|
-
}
|
|
576
|
-
```
|