@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.
- package/dist/cli-entry.js +5 -2
- package/dist/cli-entry.js.map +1 -1
- package/package.json +7 -6
- package/.github/instructions/dotnet-api-integration.instructions.md +0 -416
- package/.github/instructions/dotnet-development.instructions.md +0 -353
- package/.github/instructions/dotnet-testing.instructions.md +0 -406
- package/.github/instructions/react-development.instructions.md +0 -315
- package/.github/instructions/react-testing-optimization.instructions.md +0 -373
- package/.github/instructions/uiux.instructions.md +0 -261
|
@@ -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/)
|