@mohantn/gate-keeper 2.2.3 → 2.2.5

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.
@@ -1,406 +0,0 @@
1
- # .NET Core 8 Testing Instructions
2
-
3
- **Scope**: Unit, Integration, Test Patterns | **Version**: 2.0 | **Tags**: `testing`, `xunit`, `moq`, `integration-tests`
4
-
5
- **Related Files**: See [dotnet-development.instructions.md](./dotnet-development.instructions.md) for core patterns, [dotnet-api-integration.instructions.md](./dotnet-api-integration.instructions.md) for API patterns
6
-
7
- ## Unit Testing
8
-
9
- ### InMemory DbContext Factory
10
- ```csharp
11
- public static class InMemoryDbContextFactory {
12
- public static RetailAppDbContext CreateDbContext() {
13
- var options = new DbContextOptionsBuilder<RetailAppDbContext>()
14
- .UseInMemoryDatabase(Guid.NewGuid().ToString())
15
- .Options;
16
-
17
- var retailer = new Mock<ICurrentRetailer>();
18
- retailer.Setup(x => x.RetailerId).Returns("test");
19
-
20
- var user = new Mock<ICurrentUser>();
21
- user.Setup(x => x.UserId).Returns("test-user");
22
-
23
- return new RetailAppDbContext(options, retailer.Object, user.Object);
24
- }
25
- }
26
- ```
27
-
28
- ### Unit Test Pattern
29
- ```csharp
30
- [Fact]
31
- public async Task CreateProductAsync_WithValidInput_ReturnsProductDto() {
32
- // Arrange
33
- var dbContext = InMemoryDbContextFactory.CreateDbContext();
34
- var unitOfWork = new UnitOfWork(dbContext);
35
- var mapper = new ProductMapper();
36
- var service = new ProductService(unitOfWork, mapper);
37
-
38
- var cmd = new CreateProductCommand {
39
- Name = "Test Product",
40
- Price = 99.99m,
41
- RetailerId = "r1"
42
- };
43
-
44
- // Act
45
- var result = await service.CreateProductAsync(cmd, CancellationToken.None);
46
-
47
- // Assert
48
- Assert.NotNull(result);
49
- Assert.Equal("Test Product", result.Name);
50
- Assert.Equal(99.99m, result.Price);
51
- }
52
-
53
- [Fact]
54
- public async Task CreateProductAsync_WithNegativePrice_ThrowsException() {
55
- // Arrange
56
- var service = new ProductService(new Mock<IUnitOfWork>().Object, new Mock<ProductMapper>().Object);
57
- var cmd = new CreateProductCommand { Name = "Test", Price = -10, RetailerId = "r1" };
58
-
59
- // Act & Assert
60
- await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
61
- service.CreateProductAsync(cmd, CancellationToken.None));
62
- }
63
-
64
- [Theory]
65
- [InlineData("", 99.99, "r1")] // Empty name
66
- [InlineData("Product", 0, "r1")] // Zero price
67
- [InlineData("Product", 99.99, "")] // Empty retailer
68
- public async Task CreateProductAsync_WithInvalidInput_ThrowsValidationException(
69
- string name, decimal price, string retailerId) {
70
- // Arrange
71
- var service = new ProductService(new Mock<IUnitOfWork>().Object, new Mock<ProductMapper>().Object);
72
- var cmd = new CreateProductCommand { Name = name, Price = price, RetailerId = retailerId };
73
-
74
- // Act & Assert
75
- await Assert.ThrowsAsync<ValidationException>(() =>
76
- service.CreateProductAsync(cmd, CancellationToken.None));
77
- }
78
- ```
79
-
80
- ### Mocking with Moq
81
- ```csharp
82
- [Fact]
83
- public async Task GetProductAsync_WhenProductExists_ReturnsProduct() {
84
- // Arrange
85
- var mockRepo = new Mock<IRepository<Product>>();
86
- var expectedProduct = new Product { Id = Guid.NewGuid(), Name = "Test" };
87
-
88
- mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
89
- .ReturnsAsync(expectedProduct);
90
-
91
- var service = new ProductService(mockRepo.Object, new Mock<ProductMapper>().Object);
92
-
93
- // Act
94
- var result = await service.GetProductAsync(expectedProduct.Id, CancellationToken.None);
95
-
96
- // Assert
97
- Assert.NotNull(result);
98
- Assert.Equal("Test", result.Name);
99
- mockRepo.Verify(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Once);
100
- }
101
-
102
- [Fact]
103
- public async Task GetProductAsync_WhenProductNotFound_ThrowsNotFoundException() {
104
- // Arrange
105
- var mockRepo = new Mock<IRepository<Product>>();
106
- mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
107
- .ReturnsAsync((Product?)null);
108
-
109
- var service = new ProductService(mockRepo.Object, new Mock<ProductMapper>().Object);
110
-
111
- // Act & Assert
112
- await Assert.ThrowsAsync<NotFoundException>(() =>
113
- service.GetProductAsync(Guid.NewGuid(), CancellationToken.None));
114
- }
115
- ```
116
-
117
- ### Testing FluentValidation
118
- ```csharp
119
- [Fact]
120
- public async Task Validator_WithValidCommand_PassesValidation() {
121
- // Arrange
122
- var dbContext = InMemoryDbContextFactory.CreateDbContext();
123
- var validator = new CreateProductCommandValidator(dbContext);
124
- var command = new CreateProductCommand {
125
- Name = "Valid Product",
126
- Price = 99.99m,
127
- RetailerId = "r1"
128
- };
129
-
130
- // Act
131
- var result = await validator.ValidateAsync(command);
132
-
133
- // Assert
134
- Assert.True(result.IsValid);
135
- Assert.Empty(result.Errors);
136
- }
137
-
138
- [Fact]
139
- public async Task Validator_WithEmptyName_FailsValidation() {
140
- // Arrange
141
- var dbContext = InMemoryDbContextFactory.CreateDbContext();
142
- var validator = new CreateProductCommandValidator(dbContext);
143
- var command = new CreateProductCommand { Name = "", Price = 99.99m, RetailerId = "r1" };
144
-
145
- // Act
146
- var result = await validator.ValidateAsync(command);
147
-
148
- // Assert
149
- Assert.False(result.IsValid);
150
- Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateProductCommand.Name));
151
- }
152
-
153
- [Fact]
154
- public async Task Validator_WithDuplicateName_FailsValidation() {
155
- // Arrange
156
- var dbContext = InMemoryDbContextFactory.CreateDbContext();
157
- await dbContext.Products.AddAsync(new Product { Name = "Duplicate", Price = 50, RetailerId = "r1" });
158
- await dbContext.SaveChangesAsync();
159
-
160
- var validator = new CreateProductCommandValidator(dbContext);
161
- var command = new CreateProductCommand { Name = "Duplicate", Price = 99.99m, RetailerId = "r1" };
162
-
163
- // Act
164
- var result = await validator.ValidateAsync(command);
165
-
166
- // Assert
167
- Assert.False(result.IsValid);
168
- Assert.Contains(result.Errors, e => e.ErrorMessage.Contains("already exists"));
169
- }
170
- ```
171
-
172
- ## Integration Testing
173
-
174
- ### WebApplicationFactory Setup
175
- ```csharp
176
- public class StoreMediaApiWebApplicationFactory : WebApplicationFactory<Startup> {
177
- protected override void ConfigureWebHost(IWebHostBuilder builder) {
178
- builder.ConfigureTestServices(services => {
179
- // Replace real DB with in-memory or test DB
180
- services.RemoveAll(typeof(DbContextOptions<StoreMediaDbContext>));
181
- services.AddDbContext<StoreMediaDbContext>(options =>
182
- options.UseInMemoryDatabase("test-db-" + Guid.NewGuid()));
183
-
184
- // Override external dependencies
185
- var mockCostingApi = new Mock<ICostingApi>();
186
- services.AddScoped(_ => mockCostingApi.Object);
187
- });
188
- }
189
-
190
- public void WithStoreMediaDbContext(Action<StoreMediaDbContext> action) {
191
- using var scope = Services.CreateScope();
192
- var dbContext = scope.ServiceProvider.GetRequiredService<StoreMediaDbContext>();
193
- action(dbContext);
194
- }
195
- }
196
- ```
197
-
198
- ### Integration Test Pattern
199
- ```csharp
200
- public class ProductIntegrationTests : IClassFixture<StoreMediaApiWebApplicationFactory> {
201
- private readonly WebApplicationFactory<Startup> _factory;
202
-
203
- public ProductIntegrationTests(StoreMediaApiWebApplicationFactory factory) {
204
- _factory = factory;
205
- }
206
-
207
- [Fact]
208
- public async Task CreateProduct_WithValidRequest_Returns201Created() {
209
- // Arrange
210
- var client = _factory.CreateClient();
211
- var command = new CreateProductCommand {
212
- Name = "Integration Test Product",
213
- Price = 99.99m,
214
- RetailerId = "r1"
215
- };
216
-
217
- // Act
218
- var response = await client.PostAsJsonAsync("/api/v2/products", command);
219
-
220
- // Assert
221
- response.StatusCode.Should().Be(HttpStatusCode.Created);
222
- var result = JsonConvert.DeserializeObject<ProductDto>(
223
- await response.Content.ReadAsStringAsync());
224
- result?.Name.Should().Be("Integration Test Product");
225
- }
226
-
227
- [Fact]
228
- public async Task GetProduct_WhenExists_Returns200Ok() {
229
- // Arrange
230
- var productId = Guid.NewGuid().ToString();
231
- _factory.WithStoreMediaDbContext(ctx => {
232
- ctx.Products.Add(new Product {
233
- Id = productId,
234
- Name = "Test Product",
235
- Price = 50
236
- });
237
- ctx.SaveChanges();
238
- });
239
-
240
- var client = _factory.CreateClient();
241
-
242
- // Act
243
- var response = await client.GetAsync($"/api/v2/products/{productId}");
244
-
245
- // Assert
246
- response.StatusCode.Should().Be(HttpStatusCode.OK);
247
- var result = JsonConvert.DeserializeObject<ProductDto>(
248
- await response.Content.ReadAsStringAsync());
249
- result?.Name.Should().Be("Test Product");
250
- }
251
-
252
- [Fact]
253
- public async Task GetProduct_WhenNotFound_Returns404NotFound() {
254
- // Arrange
255
- var client = _factory.CreateClient();
256
-
257
- // Act
258
- var response = await client.GetAsync($"/api/v2/products/{Guid.NewGuid()}");
259
-
260
- // Assert
261
- response.StatusCode.Should().Be(HttpStatusCode.NotFound);
262
- }
263
- }
264
- ```
265
-
266
- ### Configuration Testing
267
- ```csharp
268
- [Fact]
269
- public void PricingConfiguration_IsSeededAndRetrievable() {
270
- _factory.WithStoreMediaDbContext(ctx => {
271
- // Arrange & Act
272
- var ofdConfig = ConfigurationEntity.GetCfgValue(
273
- ctx,
274
- ConfigurationKeys.OfdPricingConfiguration);
275
-
276
- // Assert
277
- ofdConfig.Should().NotBeNullOrEmpty();
278
- var pricingConfig = JsonConvert.DeserializeObject<PricingConfiguration>(ofdConfig!);
279
- pricingConfig?.PricingApiStrategy.Should().Be("StoreMediaApi.Pricing.OfdPricing");
280
- });
281
- }
282
- ```
283
-
284
- ## Test Coverage
285
-
286
- ### Coverage Goals
287
- - **Unit Tests**: ≥ 80% coverage for services & handlers
288
- - **Integration Tests**: Critical business flows (activation publishing, pricing, validation)
289
- - **Exclude from coverage**: Migrations, Controllers (thin layer), Seed Data, Configuration classes
290
-
291
- ### Run Coverage
292
- ```bash
293
- # Run tests with coverage
294
- dotnet test /p:CollectCoverage=true
295
-
296
- # Generate coverage report
297
- dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=opencover
298
-
299
- # View coverage in console
300
- dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=console
301
- ```
302
-
303
- ### Coverage Exclusions (.csproj)
304
- ```xml
305
- <PropertyGroup>
306
- <ExcludeByAttribute>ExcludeFromCodeCoverage;Obsolete;GeneratedCode</ExcludeByAttribute>
307
- <ExcludeByFile>**/Migrations/*.cs;**/Program.cs;**/Startup.cs</ExcludeByFile>
308
- </PropertyGroup>
309
- ```
310
-
311
- ## Essential CLI Commands
312
-
313
- ```bash
314
- # Testing
315
- dotnet test
316
- dotnet test --filter "ClassName~ProductTests"
317
- dotnet test /p:CollectCoverage=true
318
- dotnet test --logger "console;verbosity=detailed"
319
- dotnet test --no-build --verbosity normal
320
-
321
- # Watch mode for TDD
322
- dotnet watch test
323
-
324
- # Run specific test project
325
- dotnet test ./Tests/UnitTests/UnitTests.csproj
326
-
327
- # Parallel execution (faster)
328
- dotnet test --parallel
329
-
330
- # List all tests
331
- dotnet test --list-tests
332
- ```
333
-
334
- ## Test Organization
335
-
336
- ### Project Structure
337
- ```
338
- Solution/
339
- ├── src/
340
- │ ├── Core/
341
- │ ├── Application/
342
- │ ├── Infrastructure/
343
- │ └── Presentation/
344
- └── tests/
345
- ├── UnitTests/
346
- │ ├── Services/
347
- │ ├── Handlers/
348
- │ └── Validators/
349
- ├── IntegrationTests/
350
- │ ├── Api/
351
- │ ├── Database/
352
- │ └── Factories/
353
- └── TestHelpers/
354
- ├── Builders/
355
- ├── Fakes/
356
- └── Factories/
357
- ```
358
-
359
- ### Naming Conventions
360
- ```csharp
361
- // Test class naming: [ClassUnderTest]Tests
362
- public class ProductServiceTests { }
363
-
364
- // Test method naming: [Method]_[Scenario]_[Expected]
365
- [Fact]
366
- public void CreateProduct_WithNegativePrice_ThrowsException() { }
367
-
368
- [Fact]
369
- public void GetProduct_WhenProductExists_ReturnsProduct() { }
370
-
371
- [Fact]
372
- public void DeleteProduct_WhenProductNotFound_ThrowsNotFoundException() { }
373
- ```
374
-
375
- ## Quick Checklist ✅
376
-
377
- - Unit tests use InMemoryDatabase or mocks
378
- - Integration tests use WebApplicationFactory
379
- - Test methods follow naming convention: Method_Scenario_Expected
380
- - All assertions use FluentAssertions or xUnit asserts
381
- - Mocks verified with Times.Once/Times.Never
382
- - Test coverage ≥ 80% for business logic
383
- - Integration tests cover critical flows
384
- - No real database/external APIs in tests
385
- - Test isolation (each test independent)
386
- - Cleanup after tests (dispose DbContext)
387
-
388
- ## Common Mistakes ❌
389
-
390
- - Using real database in unit tests
391
- - Not disposing DbContext in tests
392
- - Tests with external dependencies (APIs, DB)
393
- - Missing test isolation (shared state between tests)
394
- - No assertions (test passes but validates nothing)
395
- - Hardcoded values instead of test data builders
396
- - Not testing edge cases and error paths
397
- - Ignoring test failures
398
- - No integration tests for critical flows
399
-
400
- ## Resources
401
-
402
- - [xUnit Documentation](https://xunit.net/)
403
- - [Moq Documentation](https://github.com/moq/moq4)
404
- - [FluentAssertions](https://fluentassertions.com/)
405
- - [WebApplicationFactory Testing](https://docs.microsoft.com/aspnet/core/test/integration-tests)
406
- - [EF Core Testing](https://docs.microsoft.com/ef/core/testing/)