@grimoire-cc/cli 0.13.1 → 0.13.3
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/package.json +1 -1
- package/packs/dev-pack/grimoire.json +9 -4
- package/packs/dev-pack/skills/grimoire.conventional-commit/SKILL.md +25 -8
- package/packs/dotnet-pack/grimoire.json +6 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/SKILL.md +293 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/anti-patterns.md +329 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/framework-guidelines.md +361 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/parameterized-testing.md +378 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-organization.md +476 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-performance.md +576 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/tunit-template.md +438 -0
- package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/xunit-template.md +303 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# TUnit Test File Template
|
|
2
|
+
|
|
3
|
+
Standard template for TUnit test files (recommended for .NET 8+ projects).
|
|
4
|
+
|
|
5
|
+
## Basic Test Class Template
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
using Microsoft.Extensions.Logging;
|
|
9
|
+
using Microsoft.Extensions.Logging.Testing;
|
|
10
|
+
using Moq;
|
|
11
|
+
using TUnit.Core;
|
|
12
|
+
using TUnit.Assertions;
|
|
13
|
+
using TUnit.Assertions.Extensions;
|
|
14
|
+
|
|
15
|
+
namespace YourProject.Tests.Services;
|
|
16
|
+
|
|
17
|
+
public class YourServiceTests : IAsyncDisposable
|
|
18
|
+
{
|
|
19
|
+
private readonly FakeLogger<YourService> _fakeLogger;
|
|
20
|
+
private readonly Mock<IDependency> _mockDependency;
|
|
21
|
+
private readonly YourService _sut; // System Under Test
|
|
22
|
+
|
|
23
|
+
public YourServiceTests()
|
|
24
|
+
{
|
|
25
|
+
_fakeLogger = new FakeLogger<YourService>();
|
|
26
|
+
_mockDependency = new Mock<IDependency>();
|
|
27
|
+
_sut = new YourService(_fakeLogger, _mockDependency.Object);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public ValueTask DisposeAsync()
|
|
31
|
+
{
|
|
32
|
+
// Cleanup resources if needed
|
|
33
|
+
return ValueTask.CompletedTask;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#region MethodName Tests
|
|
37
|
+
|
|
38
|
+
[Test]
|
|
39
|
+
public async Task MethodName_WhenConditionIsTrue_ShouldReturnExpectedResult()
|
|
40
|
+
{
|
|
41
|
+
// Arrange
|
|
42
|
+
var input = CreateTestInput();
|
|
43
|
+
_mockDependency
|
|
44
|
+
.Setup(d => d.GetDataAsync(It.IsAny<Guid>()))
|
|
45
|
+
.ReturnsAsync(expectedData);
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
var result = await _sut.MethodNameAsync(input);
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
await Assert.That(result).IsNotNull();
|
|
52
|
+
await Assert.That(result.Property).IsEqualTo(expected);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[Test]
|
|
56
|
+
public async Task MethodName_WhenDependencyFails_ShouldThrowException()
|
|
57
|
+
{
|
|
58
|
+
// Arrange
|
|
59
|
+
_mockDependency
|
|
60
|
+
.Setup(d => d.GetDataAsync(It.IsAny<Guid>()))
|
|
61
|
+
.ThrowsAsync(new InvalidOperationException("Dependency failed"));
|
|
62
|
+
|
|
63
|
+
// Act & Assert
|
|
64
|
+
await Assert.That(() => _sut.MethodNameAsync(CreateTestInput()))
|
|
65
|
+
.ThrowsException()
|
|
66
|
+
.OfType<ServiceException>();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[Test]
|
|
70
|
+
[Arguments(null)]
|
|
71
|
+
[Arguments("")]
|
|
72
|
+
[Arguments(" ")]
|
|
73
|
+
public async Task MethodName_WithInvalidInput_ShouldThrowArgumentException(
|
|
74
|
+
string? invalidInput)
|
|
75
|
+
{
|
|
76
|
+
// Act & Assert
|
|
77
|
+
await Assert.That(() => _sut.MethodNameAsync(invalidInput!))
|
|
78
|
+
.ThrowsException()
|
|
79
|
+
.OfType<ArgumentException>();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#endregion
|
|
83
|
+
|
|
84
|
+
#region Test Helpers
|
|
85
|
+
|
|
86
|
+
private static TestInput CreateTestInput() => new TestInput
|
|
87
|
+
{
|
|
88
|
+
Id = Guid.NewGuid(),
|
|
89
|
+
Name = "Test Name",
|
|
90
|
+
CreatedAt = DateTime.UtcNow
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
#endregion
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Attribute-Based Lifecycle Template
|
|
98
|
+
|
|
99
|
+
TUnit supports attribute-based setup and teardown:
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
public class DatabaseServiceTests
|
|
103
|
+
{
|
|
104
|
+
private TestDatabase _database = null!;
|
|
105
|
+
private readonly Mock<ILogger<DatabaseService>> _mockLogger;
|
|
106
|
+
private DatabaseService _sut = null!;
|
|
107
|
+
|
|
108
|
+
public DatabaseServiceTests()
|
|
109
|
+
{
|
|
110
|
+
_mockLogger = new Mock<ILogger<DatabaseService>>();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[Before(Test)]
|
|
114
|
+
public async Task SetupBeforeEachTest()
|
|
115
|
+
{
|
|
116
|
+
_database = await TestDatabase.CreateAsync();
|
|
117
|
+
_sut = new DatabaseService(_database.ConnectionString, _mockLogger.Object);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[After(Test)]
|
|
121
|
+
public async Task CleanupAfterEachTest()
|
|
122
|
+
{
|
|
123
|
+
await _database.DisposeAsync();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[Before(Class)]
|
|
127
|
+
public static async Task SetupBeforeAllTests()
|
|
128
|
+
{
|
|
129
|
+
// One-time setup for all tests in class
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[After(Class)]
|
|
133
|
+
public static async Task CleanupAfterAllTests()
|
|
134
|
+
{
|
|
135
|
+
// One-time cleanup after all tests in class
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[Test]
|
|
139
|
+
public async Task Query_WithValidSql_ReturnsResults()
|
|
140
|
+
{
|
|
141
|
+
// Test using _database and _sut
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Nested Class Organization Template
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
public class OrderServiceTests : IAsyncDisposable
|
|
150
|
+
{
|
|
151
|
+
protected readonly Mock<IOrderRepository> _mockRepository;
|
|
152
|
+
protected readonly OrderService _sut;
|
|
153
|
+
|
|
154
|
+
public OrderServiceTests()
|
|
155
|
+
{
|
|
156
|
+
_mockRepository = new Mock<IOrderRepository>();
|
|
157
|
+
_sut = new OrderService(_mockRepository.Object);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
161
|
+
|
|
162
|
+
public class CreateOrder : OrderServiceTests
|
|
163
|
+
{
|
|
164
|
+
[Test]
|
|
165
|
+
public async Task WithValidOrder_ReturnsSuccessResult()
|
|
166
|
+
{
|
|
167
|
+
var order = new Order { /* ... */ };
|
|
168
|
+
var result = await _sut.CreateOrderAsync(order);
|
|
169
|
+
await Assert.That(result.IsSuccess).IsTrue();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[Test]
|
|
173
|
+
public async Task WithNullOrder_ThrowsArgumentNullException()
|
|
174
|
+
{
|
|
175
|
+
await Assert.That(() => _sut.CreateOrderAsync(null!))
|
|
176
|
+
.ThrowsException()
|
|
177
|
+
.OfType<ArgumentNullException>();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public class UpdateOrder : OrderServiceTests
|
|
182
|
+
{
|
|
183
|
+
[Test]
|
|
184
|
+
public async Task WithValidChanges_UpdatesSuccessfully()
|
|
185
|
+
{
|
|
186
|
+
// Tests for UpdateOrder
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## ClassDataSource Fixture Template
|
|
193
|
+
|
|
194
|
+
For sharing expensive resources:
|
|
195
|
+
|
|
196
|
+
```csharp
|
|
197
|
+
// 1. Define the fixture
|
|
198
|
+
public class DatabaseFixture : IAsyncInitializable, IAsyncDisposable
|
|
199
|
+
{
|
|
200
|
+
public TestDatabase Database { get; private set; } = null!;
|
|
201
|
+
|
|
202
|
+
public async Task InitializeAsync()
|
|
203
|
+
{
|
|
204
|
+
Database = await TestDatabase.CreateAsync();
|
|
205
|
+
await Database.SeedTestDataAsync();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public async ValueTask DisposeAsync()
|
|
209
|
+
{
|
|
210
|
+
await Database.DisposeAsync();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 2. Use in test classes with ClassDataSource
|
|
215
|
+
public class OrderRepositoryTests
|
|
216
|
+
{
|
|
217
|
+
[Test]
|
|
218
|
+
[ClassDataSource<DatabaseFixture>(Shared = SharedType.Globally)]
|
|
219
|
+
public async Task GetById_ReturnsOrder(DatabaseFixture fixture)
|
|
220
|
+
{
|
|
221
|
+
using var context = new AppDbContext(fixture.Database.ConnectionString);
|
|
222
|
+
var repo = new OrderRepository(context);
|
|
223
|
+
|
|
224
|
+
var result = await repo.GetByIdAsync(TestData.ExistingOrderId);
|
|
225
|
+
|
|
226
|
+
await Assert.That(result).IsNotNull();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Parameterized Test Template
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
public class CalculatorTests
|
|
235
|
+
{
|
|
236
|
+
private readonly Calculator _sut = new();
|
|
237
|
+
|
|
238
|
+
// Simple arguments
|
|
239
|
+
[Test]
|
|
240
|
+
[Arguments(2, 3, 5)]
|
|
241
|
+
[Arguments(-1, 1, 0)]
|
|
242
|
+
[Arguments(0, 0, 0)]
|
|
243
|
+
public async Task Add_WithValidInputs_ReturnsCorrectSum(int a, int b, int expected)
|
|
244
|
+
{
|
|
245
|
+
var result = _sut.Add(a, b);
|
|
246
|
+
await Assert.That(result).IsEqualTo(expected);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// With display names for clarity
|
|
250
|
+
[Test]
|
|
251
|
+
[Arguments(10, 2, 5, DisplayName = "Simple division")]
|
|
252
|
+
[Arguments(7, 2, 3.5, DisplayName = "Division with decimal result")]
|
|
253
|
+
[Arguments(-10, 2, -5, DisplayName = "Negative numerator")]
|
|
254
|
+
public async Task Divide_WithValidInputs_ReturnsCorrectResult(
|
|
255
|
+
decimal numerator, decimal denominator, decimal expected)
|
|
256
|
+
{
|
|
257
|
+
var result = _sut.Divide(numerator, denominator);
|
|
258
|
+
await Assert.That(result).IsEqualTo(expected);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Method data source with tuples
|
|
262
|
+
[Test]
|
|
263
|
+
[MethodDataSource(nameof(GetComplexTestCases))]
|
|
264
|
+
public async Task Process_WithComplexInput_ReturnsExpected(
|
|
265
|
+
Order order, bool expectedSuccess)
|
|
266
|
+
{
|
|
267
|
+
var result = await _sut.ProcessAsync(order);
|
|
268
|
+
await Assert.That(result.IsSuccess).IsEqualTo(expectedSuccess);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public static IEnumerable<(Order, bool)> GetComplexTestCases()
|
|
272
|
+
{
|
|
273
|
+
yield return (CreateValidOrder(), true);
|
|
274
|
+
yield return (CreateInvalidOrder(), false);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Matrix testing - all combinations
|
|
278
|
+
[Test]
|
|
279
|
+
[Matrix("Small", "Medium", "Large")]
|
|
280
|
+
[Matrix("Standard", "Express")]
|
|
281
|
+
[Matrix(true, false)]
|
|
282
|
+
public async Task CalculateShipping_ReturnsValidPrice(
|
|
283
|
+
string size, string method, bool isInternational)
|
|
284
|
+
{
|
|
285
|
+
var result = await _sut.CalculateShippingAsync(size, method, isInternational);
|
|
286
|
+
await Assert.That(result).IsGreaterThan(0);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Logger Testing Template
|
|
292
|
+
|
|
293
|
+
```csharp
|
|
294
|
+
public class ServiceWithLoggingTests
|
|
295
|
+
{
|
|
296
|
+
private readonly FakeLogger<MyService> _fakeLogger;
|
|
297
|
+
private readonly MyService _sut;
|
|
298
|
+
|
|
299
|
+
public ServiceWithLoggingTests()
|
|
300
|
+
{
|
|
301
|
+
_fakeLogger = new FakeLogger<MyService>();
|
|
302
|
+
_sut = new MyService(_fakeLogger);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
[Test]
|
|
306
|
+
public async Task ProcessOrder_LogsOrderId()
|
|
307
|
+
{
|
|
308
|
+
// Arrange
|
|
309
|
+
var orderId = 123;
|
|
310
|
+
|
|
311
|
+
// Act
|
|
312
|
+
await _sut.ProcessOrderAsync(orderId);
|
|
313
|
+
|
|
314
|
+
// Assert - Verify structured log properties
|
|
315
|
+
var logEntry = _fakeLogger.Collector.GetSnapshot()
|
|
316
|
+
.Single(r => r.Level == LogLevel.Information);
|
|
317
|
+
|
|
318
|
+
await Assert.That(logEntry.StructuredState).IsNotNull();
|
|
319
|
+
var state = logEntry.StructuredState!.ToDictionary(x => x.Key, x => x.Value);
|
|
320
|
+
|
|
321
|
+
await Assert.That(state).ContainsKey("OrderId");
|
|
322
|
+
await Assert.That(state["OrderId"]).IsEqualTo("123");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
[Test]
|
|
326
|
+
public async Task ProcessOrder_WhenFails_LogsError()
|
|
327
|
+
{
|
|
328
|
+
// Act
|
|
329
|
+
await Assert.That(() => _sut.ProcessOrderAsync(-1))
|
|
330
|
+
.ThrowsException();
|
|
331
|
+
|
|
332
|
+
// Assert
|
|
333
|
+
var errorLog = _fakeLogger.Collector.GetSnapshot()
|
|
334
|
+
.SingleOrDefault(r => r.Level == LogLevel.Error);
|
|
335
|
+
|
|
336
|
+
await Assert.That(errorLog).IsNotNull();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Fluent Assertion Chaining Template
|
|
342
|
+
|
|
343
|
+
```csharp
|
|
344
|
+
[Test]
|
|
345
|
+
public async Task ComplexResult_MeetsAllExpectations()
|
|
346
|
+
{
|
|
347
|
+
var result = await _sut.ProcessAsync(input);
|
|
348
|
+
|
|
349
|
+
// Chain multiple assertions
|
|
350
|
+
await Assert.That(result)
|
|
351
|
+
.IsNotNull()
|
|
352
|
+
.And.HasProperty(r => r.Items)
|
|
353
|
+
.And.HasCount().GreaterThan(0);
|
|
354
|
+
|
|
355
|
+
// Collection assertions
|
|
356
|
+
await Assert.That(result.Items).HasCount(3);
|
|
357
|
+
await Assert.That(result.Items).Contains(expectedItem);
|
|
358
|
+
await Assert.That(result.Items).IsEquivalentTo(expectedItems);
|
|
359
|
+
|
|
360
|
+
// String assertions
|
|
361
|
+
await Assert.That(result.Message).StartsWith("Success:");
|
|
362
|
+
await Assert.That(result.Message).Contains("processed");
|
|
363
|
+
|
|
364
|
+
// Numeric assertions with tolerance
|
|
365
|
+
await Assert.That(result.Total).IsEqualTo(expected).Within(0.001);
|
|
366
|
+
await Assert.That(result.ProcessedAt)
|
|
367
|
+
.IsEqualTo(DateTime.UtcNow)
|
|
368
|
+
.Within(TimeSpan.FromSeconds(1));
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Timeout and Retry Template
|
|
373
|
+
|
|
374
|
+
```csharp
|
|
375
|
+
public class ResilientTests
|
|
376
|
+
{
|
|
377
|
+
[Test]
|
|
378
|
+
[Timeout(5000)] // 5 seconds max
|
|
379
|
+
public async Task LongRunningOperation_CompletesWithinTimeout()
|
|
380
|
+
{
|
|
381
|
+
var result = await _sut.SlowOperationAsync();
|
|
382
|
+
await Assert.That(result).IsNotNull();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
[Test]
|
|
386
|
+
[Retry(3)] // Retry up to 3 times if fails
|
|
387
|
+
public async Task FlakeyExternalService_EventuallySucceeds()
|
|
388
|
+
{
|
|
389
|
+
var result = await _sut.CallExternalServiceAsync();
|
|
390
|
+
await Assert.That(result.IsSuccess).IsTrue();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
[Test]
|
|
394
|
+
[Timeout(10000)]
|
|
395
|
+
[Retry(2)]
|
|
396
|
+
public async Task CombinedTimeoutAndRetry()
|
|
397
|
+
{
|
|
398
|
+
// Has 10 seconds to complete, will retry twice on failure
|
|
399
|
+
var result = await _sut.UnreliableOperationAsync();
|
|
400
|
+
await Assert.That(result).IsNotNull();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Parallel Control Template
|
|
406
|
+
|
|
407
|
+
```csharp
|
|
408
|
+
// Tests that must run sequentially
|
|
409
|
+
[NotInParallel]
|
|
410
|
+
public class SequentialTests
|
|
411
|
+
{
|
|
412
|
+
[Test]
|
|
413
|
+
public async Task Test1_ModifiesSharedResource() { }
|
|
414
|
+
|
|
415
|
+
[Test]
|
|
416
|
+
public async Task Test2_AlsoModifiesSharedResource() { }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Tests that share a parallel group (run sequentially within group)
|
|
420
|
+
[ParallelGroup("Database")]
|
|
421
|
+
public class OrderRepositoryTests { }
|
|
422
|
+
|
|
423
|
+
[ParallelGroup("Database")]
|
|
424
|
+
public class CustomerRepositoryTests { }
|
|
425
|
+
|
|
426
|
+
// Custom parallel limit
|
|
427
|
+
[ParallelLimiter<MaxParallel3>]
|
|
428
|
+
public class ResourceIntensiveTests
|
|
429
|
+
{
|
|
430
|
+
[Test]
|
|
431
|
+
public async Task Test1() { }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
public class MaxParallel3 : IParallelLimit
|
|
435
|
+
{
|
|
436
|
+
public int Limit => 3;
|
|
437
|
+
}
|
|
438
|
+
```
|