@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,476 @@
1
+ # Test Organization
2
+
3
+ Guidelines for organizing large test suites effectively.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Folder and Namespace Structure](#folder-and-namespace-structure)
8
+ - [File Naming Conventions](#file-naming-conventions)
9
+ - [Nested Classes for Organization](#nested-classes-for-organization)
10
+ - [Test Categories and Traits](#test-categories-and-traits)
11
+ - [Test Collections and Shared Fixtures](#test-collections-and-shared-fixtures)
12
+ - [Controlling Parallel Execution](#controlling-parallel-execution)
13
+ - [Project Structure for Large Solutions](#project-structure-for-large-solutions)
14
+ - [When to Split Test Projects](#when-to-split-test-projects)
15
+
16
+ ---
17
+
18
+ ## Folder and Namespace Structure
19
+
20
+ Mirror your source code structure in the test project:
21
+
22
+ ```plain
23
+ src/
24
+ ├── MyApp.Domain/
25
+ │ ├── Entities/
26
+ │ │ └── Order.cs
27
+ │ └── Services/
28
+ │ ├── OrderService.cs
29
+ │ └── PaymentService.cs
30
+ ├── MyApp.Infrastructure/
31
+ │ └── Repositories/
32
+ │ └── OrderRepository.cs
33
+ └── MyApp.Api/
34
+ └── Controllers/
35
+ └── OrderController.cs
36
+
37
+ tests/
38
+ ├── MyApp.Domain.Tests/
39
+ │ ├── Entities/
40
+ │ │ └── OrderTests.cs
41
+ │ └── Services/
42
+ │ ├── OrderServiceTests.cs
43
+ │ └── PaymentServiceTests.cs
44
+ ├── MyApp.Infrastructure.Tests/
45
+ │ └── Repositories/
46
+ │ └── OrderRepositoryTests.cs
47
+ └── MyApp.Api.Tests/
48
+ └── Controllers/
49
+ └── OrderControllerTests.cs
50
+ ```
51
+
52
+ ---
53
+
54
+ ## File Naming Conventions
55
+
56
+ | Source File | Test File | Pattern |
57
+ | ------------- | ----------- | --------- |
58
+ | `OrderService.cs` | `OrderServiceTests.cs` | `{ClassName}Tests.cs` |
59
+ | `IOrderRepository.cs` | `OrderRepositoryTests.cs` | Test the implementation, not interface |
60
+ | `OrderValidator.cs` | `OrderValidatorTests.cs` | `{ClassName}Tests.cs` |
61
+
62
+ ---
63
+
64
+ ## Nested Classes for Organization
65
+
66
+ Use nested classes to group related tests within a single test file.
67
+
68
+ ### xUnit - Nested Classes
69
+
70
+ ```csharp
71
+ public class OrderServiceTests : IDisposable
72
+ {
73
+ private readonly Mock<IOrderRepository> _mockRepository;
74
+ private readonly OrderService _sut;
75
+
76
+ public OrderServiceTests()
77
+ {
78
+ _mockRepository = new Mock<IOrderRepository>();
79
+ _sut = new OrderService(_mockRepository.Object);
80
+ }
81
+
82
+ public void Dispose() { }
83
+
84
+ // Group tests by method being tested
85
+ public class CreateOrder : OrderServiceTests
86
+ {
87
+ [Fact]
88
+ public async Task WithValidOrder_ReturnsSuccessResult()
89
+ {
90
+ // Inherits _sut and _mockRepository from parent
91
+ var order = new Order { /* ... */ };
92
+ var result = await _sut.CreateOrderAsync(order);
93
+ Assert.True(result.IsSuccess);
94
+ }
95
+
96
+ [Fact]
97
+ public async Task WithNullOrder_ThrowsArgumentNullException()
98
+ {
99
+ await Assert.ThrowsAsync<ArgumentNullException>(
100
+ () => _sut.CreateOrderAsync(null!));
101
+ }
102
+
103
+ [Fact]
104
+ public async Task WithDuplicateOrderNumber_ThrowsConflictException()
105
+ {
106
+ _mockRepository.Setup(r => r.ExistsAsync(It.IsAny<string>()))
107
+ .ReturnsAsync(true);
108
+
109
+ await Assert.ThrowsAsync<ConflictException>(
110
+ () => _sut.CreateOrderAsync(new Order { OrderNumber = "ORD-001" }));
111
+ }
112
+ }
113
+
114
+ public class UpdateOrder : OrderServiceTests
115
+ {
116
+ [Fact]
117
+ public async Task WithValidChanges_UpdatesSuccessfully() { }
118
+
119
+ [Fact]
120
+ public async Task WithNonExistentOrder_ThrowsNotFoundException() { }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### TUnit - Nested Classes
126
+
127
+ ```csharp
128
+ public class OrderServiceTests : IAsyncDisposable
129
+ {
130
+ protected readonly Mock<IOrderRepository> _mockRepository;
131
+ protected readonly OrderService _sut;
132
+
133
+ public OrderServiceTests()
134
+ {
135
+ _mockRepository = new Mock<IOrderRepository>();
136
+ _sut = new OrderService(_mockRepository.Object);
137
+ }
138
+
139
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
140
+
141
+ public class CreateOrder : OrderServiceTests
142
+ {
143
+ [Test]
144
+ public async Task WithValidOrder_ReturnsSuccessResult()
145
+ {
146
+ var order = new Order { /* ... */ };
147
+ var result = await _sut.CreateOrderAsync(order);
148
+ await Assert.That(result.IsSuccess).IsTrue();
149
+ }
150
+
151
+ [Test]
152
+ public async Task WithNullOrder_ThrowsArgumentNullException()
153
+ {
154
+ await Assert.That(() => _sut.CreateOrderAsync(null!))
155
+ .ThrowsException()
156
+ .OfType<ArgumentNullException>();
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ **Test Output with Nested Classes:**
163
+
164
+ ```plain
165
+ OrderServiceTests+CreateOrder.WithValidOrder_ReturnsSuccessResult ✓
166
+ OrderServiceTests+CreateOrder.WithNullOrder_ThrowsArgumentNullException ✓
167
+ OrderServiceTests+UpdateOrder.WithValidChanges_UpdatesSuccessfully ✓
168
+ ```
169
+
170
+ ### Alternative: Region-Based Organization
171
+
172
+ For simpler cases, use `#region` to organize tests:
173
+
174
+ ```csharp
175
+ public class OrderServiceTests : IDisposable
176
+ {
177
+ private readonly OrderService _sut;
178
+
179
+ public OrderServiceTests()
180
+ {
181
+ _sut = new OrderService(/* ... */);
182
+ }
183
+
184
+ public void Dispose() { }
185
+
186
+ #region CreateOrder Tests
187
+
188
+ [Fact]
189
+ public async Task CreateOrder_WithValidData_ReturnsSuccess() { }
190
+
191
+ [Fact]
192
+ public async Task CreateOrder_WithNullOrder_ThrowsArgumentNullException() { }
193
+
194
+ #endregion
195
+
196
+ #region UpdateOrder Tests
197
+
198
+ [Fact]
199
+ public async Task UpdateOrder_WithValidChanges_UpdatesSuccessfully() { }
200
+
201
+ #endregion
202
+
203
+ #region Test Helpers
204
+
205
+ private static Order CreateValidOrder() => new Order { /* ... */ };
206
+
207
+ #endregion
208
+ }
209
+ ```
210
+
211
+ **When to use each:**
212
+
213
+ - **Nested Classes**: Different setup/teardown needs, shared state within group, better test explorer grouping
214
+ - **Regions**: Same setup for all tests, simpler structure, IDE collapsibility
215
+
216
+ ---
217
+
218
+ ## Test Categories and Traits
219
+
220
+ ### xUnit - Traits
221
+
222
+ ```csharp
223
+ public class OrderServiceTests
224
+ {
225
+ [Fact]
226
+ [Trait("Category", "Unit")]
227
+ public async Task CreateOrder_WithValidData_Succeeds() { }
228
+
229
+ [Fact]
230
+ [Trait("Category", "Integration")]
231
+ [Trait("Database", "Required")]
232
+ public async Task CreateOrder_PersistsToDatabase() { }
233
+
234
+ [Fact]
235
+ [Trait("Category", "Slow")]
236
+ public async Task ProcessBulkOrders_HandlesLargeDataset() { }
237
+
238
+ [Fact]
239
+ [Trait("Bug", "JIRA-1234")]
240
+ public async Task CreateOrder_WithSpecialCharacters_DoesNotFail() { }
241
+ }
242
+
243
+ // Run specific categories:
244
+ // dotnet test --filter "Category=Unit"
245
+ // dotnet test --filter "Category!=Slow"
246
+ // dotnet test --filter "Bug=JIRA-1234"
247
+ ```
248
+
249
+ ### TUnit - Categories
250
+
251
+ ```csharp
252
+ public class OrderServiceTests
253
+ {
254
+ [Test]
255
+ [Category("Unit")]
256
+ public async Task CreateOrder_WithValidData_Succeeds() { }
257
+
258
+ [Test]
259
+ [Category("Integration")]
260
+ [Category("Database")]
261
+ public async Task CreateOrder_PersistsToDatabase() { }
262
+
263
+ [Test]
264
+ [Property("Bug", "JIRA-1234")]
265
+ public async Task CreateOrder_WithSpecialCharacters_DoesNotFail() { }
266
+ }
267
+ ```
268
+
269
+ ### Common Category Conventions
270
+
271
+ ```csharp
272
+ [Trait("Category", "Unit")] // Fast, isolated, no external dependencies
273
+ [Trait("Category", "Integration")] // Requires external resources (DB, API)
274
+ [Trait("Category", "E2E")] // End-to-end tests
275
+ [Trait("Category", "Smoke")] // Quick sanity checks for deployments
276
+ [Trait("Category", "Slow")] // Tests that take >1 second
277
+ [Trait("Category", "Flaky")] // Known intermittent failures
278
+
279
+ // CI/CD pipeline examples:
280
+ // PR builds: dotnet test --filter "Category=Unit"
281
+ // Nightly builds: dotnet test --filter "Category=Unit|Category=Integration"
282
+ // Release builds: dotnet test (all tests)
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Test Collections and Shared Fixtures
288
+
289
+ ### xUnit - Collections for Shared Context
290
+
291
+ Use collections when multiple test classes share expensive setup:
292
+
293
+ ```csharp
294
+ // 1. Define the shared fixture
295
+ public class DatabaseFixture : IAsyncLifetime
296
+ {
297
+ public TestDatabase Database { get; private set; } = null!;
298
+ public string ConnectionString => Database.ConnectionString;
299
+
300
+ public async Task InitializeAsync()
301
+ {
302
+ Database = await TestDatabase.CreateAsync();
303
+ await Database.MigrateAsync();
304
+ await Database.SeedTestDataAsync();
305
+ }
306
+
307
+ public async Task DisposeAsync()
308
+ {
309
+ await Database.DisposeAsync();
310
+ }
311
+ }
312
+
313
+ // 2. Define the collection
314
+ [CollectionDefinition("Database")]
315
+ public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
316
+ {
317
+ // Marker class, no code needed
318
+ }
319
+
320
+ // 3. Use in test classes
321
+ [Collection("Database")]
322
+ public class OrderRepositoryTests
323
+ {
324
+ private readonly DatabaseFixture _fixture;
325
+
326
+ public OrderRepositoryTests(DatabaseFixture fixture)
327
+ {
328
+ _fixture = fixture;
329
+ }
330
+
331
+ [Fact]
332
+ public async Task GetById_WithExistingOrder_ReturnsOrder()
333
+ {
334
+ using var context = new AppDbContext(_fixture.ConnectionString);
335
+ var repo = new OrderRepository(context);
336
+
337
+ var result = await repo.GetByIdAsync(TestData.ExistingOrderId);
338
+
339
+ Assert.NotNull(result);
340
+ }
341
+ }
342
+
343
+ [Collection("Database")]
344
+ public class CustomerRepositoryTests
345
+ {
346
+ private readonly DatabaseFixture _fixture;
347
+
348
+ public CustomerRepositoryTests(DatabaseFixture fixture)
349
+ {
350
+ _fixture = fixture;
351
+ // Same database instance shared across both test classes
352
+ }
353
+ }
354
+ ```
355
+
356
+ ### TUnit - ClassDataSource for Shared Fixtures
357
+
358
+ ```csharp
359
+ public class DatabaseFixture : IAsyncInitializable, IAsyncDisposable
360
+ {
361
+ public TestDatabase Database { get; private set; } = null!;
362
+
363
+ public async Task InitializeAsync()
364
+ {
365
+ Database = await TestDatabase.CreateAsync();
366
+ await Database.MigrateAsync();
367
+ }
368
+
369
+ public async ValueTask DisposeAsync()
370
+ {
371
+ await Database.DisposeAsync();
372
+ }
373
+ }
374
+
375
+ public class OrderRepositoryTests
376
+ {
377
+ [Test]
378
+ [ClassDataSource<DatabaseFixture>(Shared = SharedType.Globally)]
379
+ public async Task GetById_WithExistingOrder_ReturnsOrder(DatabaseFixture fixture)
380
+ {
381
+ using var context = new AppDbContext(fixture.Database.ConnectionString);
382
+ var repo = new OrderRepository(context);
383
+
384
+ var result = await repo.GetByIdAsync(TestData.ExistingOrderId);
385
+
386
+ await Assert.That(result).IsNotNull();
387
+ }
388
+ }
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Controlling Parallel Execution
394
+
395
+ ### xUnit
396
+
397
+ ```csharp
398
+ // Disable for entire assembly (AssemblyInfo.cs)
399
+ [assembly: CollectionBehavior(DisableTestParallelization = true)]
400
+
401
+ // Limit parallel threads
402
+ [assembly: CollectionBehavior(MaxParallelThreads = 4)]
403
+
404
+ // Disable for specific collection
405
+ [CollectionDefinition("Sequential", DisableParallelization = true)]
406
+ public class SequentialCollection { }
407
+
408
+ [Collection("Sequential")]
409
+ public class TestsThatCannotRunInParallel { }
410
+ ```
411
+
412
+ ### TUnit
413
+
414
+ ```csharp
415
+ // Sequential within a class
416
+ [NotInParallel]
417
+ public class SequentialTests { }
418
+
419
+ // Named parallel group (tests in same group run sequentially)
420
+ [ParallelGroup("DatabaseTests")]
421
+ public class OrderRepositoryTests { }
422
+
423
+ [ParallelGroup("DatabaseTests")]
424
+ public class CustomerRepositoryTests { }
425
+
426
+ // Custom parallelism limit
427
+ [ParallelLimiter<MaxParallel3>]
428
+ public class ResourceIntensiveTests { }
429
+
430
+ public class MaxParallel3 : IParallelLimit
431
+ {
432
+ public int Limit => 3;
433
+ }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Project Structure for Large Solutions
439
+
440
+ ```plain
441
+ tests/
442
+ ├── Unit/
443
+ │ ├── MyApp.Domain.Tests/ # Domain logic unit tests
444
+ │ ├── MyApp.Application.Tests/ # Application/use case tests
445
+ │ └── MyApp.Api.Tests/ # Controller unit tests
446
+
447
+ ├── Integration/
448
+ │ ├── MyApp.Infrastructure.Tests/ # Repository, external service tests
449
+ │ └── MyApp.Api.Integration.Tests/ # API tests with real database
450
+
451
+ ├── E2E/
452
+ │ └── MyApp.E2E.Tests/ # Full end-to-end tests
453
+
454
+ └── Shared/
455
+ └── MyApp.Tests.Common/ # Shared fixtures, builders, utilities
456
+ ├── Fixtures/
457
+ │ ├── DatabaseFixture.cs
458
+ │ └── ApiFixture.cs
459
+ ├── Builders/
460
+ │ ├── OrderBuilder.cs
461
+ │ └── CustomerBuilder.cs
462
+ └── Extensions/
463
+ └── AssertionExtensions.cs
464
+ ```
465
+
466
+ ---
467
+
468
+ ## When to Split Test Projects
469
+
470
+ | Scenario | Recommendation |
471
+ | ---------- | ---------------- |
472
+ | <500 tests total | Single test project per source project |
473
+ | 500-2000 tests | Split Unit vs Integration |
474
+ | >2000 tests | Split by test type AND by domain area |
475
+ | Different test runners needed | Separate projects |
476
+ | Different framework versions | Separate projects |