@grimoire-cc/cli 0.13.0 → 0.13.2

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.
@@ -0,0 +1,576 @@
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
+ ```