729 lines
23 KiB
Markdown
729 lines
23 KiB
Markdown
---
|
|
name: dotnet-senior-tester
|
|
description: Thực hiện kiểm thử toàn diện cho .NET Microservices (xUnit, NSubstitute, TestServer, Polly) theo mô hình Test Pyramid. Use for unit testing, integration testing, functional testing, resilience testing, và test coverage analysis.
|
|
compatibility: ".NET 10+, xUnit 2.x, NSubstitute 5.x, FluentAssertions, Testcontainers, Microsoft.AspNetCore.TestHost, Polly"
|
|
metadata:
|
|
author: Velik Ho
|
|
version: "1.0"
|
|
role: Senior SDET (Software Development Engineer in Test)
|
|
---
|
|
|
|
# Workflow Kiểm Thử .NET (Senior Tester)
|
|
|
|
Bạn là một **Senior SDET** chuyên về .NET. Nhiệm vụ của bạn là đảm bảo chất lượng code thông qua 4 tầng kiểm thử nghiêm ngặt theo mô hình Test Pyramid. Bạn **KHÔNG ĐƯỢC** viết test sơ sài hoặc bỏ qua edge cases.
|
|
|
|
## When to Use This Skill / Khi Nào Sử Dụng
|
|
|
|
Use this skill when:
|
|
- Writing unit tests for MediatR handlers / Viết unit tests cho MediatR handlers
|
|
- Testing domain entities và aggregate roots / Kiểm thử domain entities và aggregate roots
|
|
- Creating integration tests with database / Tạo integration tests với database
|
|
- Setting up functional API tests / Cài đặt functional API tests
|
|
- Testing Polly resilience policies / Kiểm thử Polly resilience policies
|
|
- Conducting test review / Đánh giá code test
|
|
|
|
## Core Concepts / Khái Niệm Cốt Lõi
|
|
|
|
### Test Pyramid / Kim Tự Tháp Testing
|
|
|
|
```
|
|
/\
|
|
/ \ E2E/Functional Tests (ít test, chậm, đắt)
|
|
/----\
|
|
/ \ Integration Tests (trung bình)
|
|
/--------\
|
|
/ \ Unit Tests (nhiều test, nhanh, rẻ)
|
|
--------------
|
|
```
|
|
|
|
| Tầng | Phạm vi | Tốc độ | Dependencies |
|
|
|------|---------|--------|--------------|
|
|
| **Unit** | Single class/method | Milliseconds | Mocked |
|
|
| **Integration** | Multiple components + DB | Seconds | Real/Containerized |
|
|
| **Functional** | Full API workflow | Seconds | Real services |
|
|
| **Resilience** | Fault tolerance | Seconds | Mocked failures |
|
|
|
|
### Test Project Naming Convention
|
|
|
|
```
|
|
tests/
|
|
├── ServiceName.UnitTests/ # Kiểm thử đơn vị
|
|
│ ├── Handlers/
|
|
│ ├── Domain/
|
|
│ └── Validators/
|
|
├── ServiceName.IntegrationTests/ # Kiểm thử tích hợp
|
|
│ ├── Fixtures/
|
|
│ └── Repositories/
|
|
├── ServiceName.FunctionalTests/ # Kiểm thử chức năng
|
|
│ └── ApiTests/
|
|
└── ServiceName.ResilienceTests/ # Kiểm thử khả năng phục hồi
|
|
└── Policies/
|
|
```
|
|
|
|
---
|
|
|
|
## Giai đoạn 1: Unit Testing (Tốc độ cao - Cô lập hoàn toàn)
|
|
|
|
**Mục tiêu**: Kiểm tra logic nghiệp vụ cốt lõi mà không phụ thuộc vào Database, API hay File System.
|
|
|
|
### 1.1 Domain Layer Testing
|
|
|
|
#### Kiểm thử Aggregate Root
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Tests for Order aggregate root invariants.
|
|
/// VI: Kiểm thử các quy tắc bất biến của Order aggregate root.
|
|
/// </summary>
|
|
public class OrderAggregateTests
|
|
{
|
|
[Fact]
|
|
public void AddItem_ValidItem_IncreasesTotalAmount()
|
|
{
|
|
// Arrange
|
|
var order = Order.Create("user-123", CreateAddress());
|
|
var productId = Guid.NewGuid();
|
|
|
|
// Act
|
|
order.AddItem(productId, quantity: 2, unitPrice: 10.00m);
|
|
|
|
// Assert
|
|
order.TotalAmount.Should().Be(20.00m);
|
|
order.OrderItems.Should().HaveCount(1);
|
|
order.DomainEvents.Should().ContainSingle(e => e is OrderItemAddedEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void Submit_EmptyOrder_ThrowsDomainException()
|
|
{
|
|
// Arrange
|
|
var order = Order.Create("user-123", CreateAddress());
|
|
|
|
// Act & Assert
|
|
var act = () => order.Submit();
|
|
act.Should().Throw<DomainException>()
|
|
.WithMessage("*cannot submit empty*");
|
|
}
|
|
|
|
private static Address CreateAddress() =>
|
|
Address.Create("123 Main St", "City", "State", "12345", "VN");
|
|
}
|
|
```
|
|
|
|
#### Kiểm thử Value Object
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Tests for Money value object immutability and equality.
|
|
/// VI: Kiểm thử tính bất biến và so sánh bằng giá trị của Money value object.
|
|
/// </summary>
|
|
public class MoneyValueObjectTests
|
|
{
|
|
[Fact]
|
|
public void Add_SameCurrency_ReturnsNewInstance()
|
|
{
|
|
// Arrange
|
|
var money1 = Money.Create(100, "VND");
|
|
var money2 = Money.Create(50, "VND");
|
|
|
|
// Act
|
|
var result = money1.Add(money2);
|
|
|
|
// Assert
|
|
result.Amount.Should().Be(150);
|
|
result.Should().NotBeSameAs(money1); // Immutability check
|
|
}
|
|
|
|
[Fact]
|
|
public void Add_DifferentCurrency_ThrowsException()
|
|
{
|
|
// Arrange
|
|
var vnd = Money.Create(100, "VND");
|
|
var usd = Money.Create(10, "USD");
|
|
|
|
// Act & Assert
|
|
var act = () => vnd.Add(usd);
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.WithMessage("*currency mismatch*");
|
|
}
|
|
|
|
[Fact]
|
|
public void Equals_SameValues_ReturnsTrue()
|
|
{
|
|
// Arrange & Act
|
|
var money1 = Money.Create(100, "VND");
|
|
var money2 = Money.Create(100, "VND");
|
|
|
|
// Assert
|
|
money1.Should().Be(money2);
|
|
money1.GetHashCode().Should().Be(money2.GetHashCode());
|
|
}
|
|
}
|
|
```
|
|
|
|
### 1.2 Application Layer Testing
|
|
|
|
#### Kiểm thử Command Handler
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Unit test for CreateOrderCommandHandler using NSubstitute.
|
|
/// VI: Unit test cho CreateOrderCommandHandler sử dụng NSubstitute.
|
|
/// </summary>
|
|
public class CreateOrderCommandHandlerTests
|
|
{
|
|
private readonly IOrderRepository _orderRepository;
|
|
private readonly ILogger<CreateOrderCommandHandler> _logger;
|
|
private readonly CreateOrderCommandHandler _handler;
|
|
|
|
public CreateOrderCommandHandlerTests()
|
|
{
|
|
// EN: Create mocks with NSubstitute
|
|
// VI: Tạo mocks với NSubstitute
|
|
_orderRepository = Substitute.For<IOrderRepository>();
|
|
_logger = Substitute.For<ILogger<CreateOrderCommandHandler>>();
|
|
|
|
_handler = new CreateOrderCommandHandler(_orderRepository, _logger);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidCommand_CreatesOrderAndReturnsId()
|
|
{
|
|
// Arrange
|
|
var command = new CreateOrderCommand(
|
|
UserId: "user-123",
|
|
ShippingAddress: new AddressDto("123 Main St", "City", "State", "12345", "VN"),
|
|
Items: new List<OrderItemDto>
|
|
{
|
|
new(ProductId: Guid.NewGuid(), Quantity: 2, UnitPrice: 10.00m)
|
|
});
|
|
|
|
_orderRepository.AddAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
|
|
.Returns(callInfo => callInfo.Arg<Order>());
|
|
_orderRepository.UnitOfWork.SaveChangesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(1);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.OrderId.Should().NotBeEmpty();
|
|
|
|
// Verify repository called exactly once
|
|
await _orderRepository.Received(1).AddAsync(
|
|
Arg.Is<Order>(o => o.UserId == "user-123"),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_EmptyItems_ThrowsValidationException()
|
|
{
|
|
// Arrange
|
|
var command = new CreateOrderCommand(
|
|
UserId: "user-123",
|
|
ShippingAddress: new AddressDto("123 Main St", "City", "State", "12345", "VN"),
|
|
Items: new List<OrderItemDto>()); // Empty!
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<ValidationException>(() =>
|
|
_handler.Handle(command, CancellationToken.None));
|
|
|
|
// Verify repository was NOT called
|
|
await _orderRepository.DidNotReceive().AddAsync(
|
|
Arg.Any<Order>(),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Giai đoạn 2: Integration Testing (Kiểm tra sự kết hợp)
|
|
|
|
**Mục tiêu**: Đảm bảo code tương tác đúng với Infrastructure (Database, EF Core, External Services).
|
|
|
|
### 2.1 Database Fixture với Testcontainers
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: PostgreSQL container fixture for integration tests.
|
|
/// VI: Fixture container PostgreSQL cho integration tests.
|
|
/// </summary>
|
|
public class PostgresFixture : IAsyncLifetime
|
|
{
|
|
private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
|
|
.WithImage("postgres:16-alpine")
|
|
.WithDatabase("testdb")
|
|
.WithUsername("test")
|
|
.WithPassword("test123")
|
|
.Build();
|
|
|
|
public string ConnectionString => _container.GetConnectionString();
|
|
public ApplicationDbContext DbContext { get; private set; } = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await _container.StartAsync();
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseNpgsql(ConnectionString)
|
|
.Options;
|
|
|
|
DbContext = new ApplicationDbContext(options);
|
|
await DbContext.Database.MigrateAsync();
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await DbContext.DisposeAsync();
|
|
await _container.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Collection definition for shared database fixture.
|
|
/// VI: Định nghĩa collection cho fixture database dùng chung.
|
|
/// </summary>
|
|
[CollectionDefinition("Database")]
|
|
public class DatabaseCollection : ICollectionFixture<PostgresFixture>
|
|
{
|
|
// This class has no code, just decorates for xUnit
|
|
}
|
|
```
|
|
|
|
### 2.2 Repository Integration Tests
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Integration tests for OrderRepository with real PostgreSQL.
|
|
/// VI: Integration tests cho OrderRepository với PostgreSQL thật.
|
|
/// </summary>
|
|
[Collection("Database")]
|
|
public class OrderRepositoryIntegrationTests : IClassFixture<PostgresFixture>
|
|
{
|
|
private readonly PostgresFixture _fixture;
|
|
private readonly OrderRepository _repository;
|
|
|
|
public OrderRepositoryIntegrationTests(PostgresFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_repository = new OrderRepository(_fixture.DbContext);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddAsync_ValidOrder_PersistsWithAllRelatedEntities()
|
|
{
|
|
// Arrange
|
|
var order = Order.Create("user-123", Address.Create("St", "City", "State", "12345", "VN"));
|
|
order.AddItem(Guid.NewGuid(), 2, 10.00m);
|
|
order.AddItem(Guid.NewGuid(), 1, 25.00m);
|
|
|
|
// Act
|
|
await _repository.AddAsync(order);
|
|
await _repository.UnitOfWork.SaveChangesAsync();
|
|
|
|
// Assert - Query directly to verify persistence
|
|
var savedOrder = await _fixture.DbContext.Orders
|
|
.Include(o => o.OrderItems)
|
|
.FirstOrDefaultAsync(o => o.Id == order.Id);
|
|
|
|
savedOrder.Should().NotBeNull();
|
|
savedOrder!.OrderItems.Should().HaveCount(2);
|
|
savedOrder.TotalAmount.Should().Be(45.00m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetWithItemsAsync_ExistingOrder_LoadsEagerRelationships()
|
|
{
|
|
// Arrange - Seed data
|
|
var order = Order.Create("user-456", Address.Create("St", "City", "State", "12345", "VN"));
|
|
order.AddItem(Guid.NewGuid(), 3, 15.00m);
|
|
await _repository.AddAsync(order);
|
|
await _repository.UnitOfWork.SaveChangesAsync();
|
|
|
|
// Act - Clear tracking to force fresh load
|
|
_fixture.DbContext.ChangeTracker.Clear();
|
|
var loadedOrder = await _repository.GetWithItemsAsync(order.Id);
|
|
|
|
// Assert
|
|
loadedOrder.Should().NotBeNull();
|
|
loadedOrder!.OrderItems.Should().NotBeEmpty();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Giai đoạn 3: Functional / API Testing (End-to-End)
|
|
|
|
**Mục tiêu**: Kiểm tra luồng hoạt động từ góc độ người dùng (API Consumer).
|
|
|
|
### 3.1 Custom WebApplicationFactory
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Web application factory for functional API tests.
|
|
/// VI: Web application factory cho functional API tests.
|
|
/// </summary>
|
|
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
|
{
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
{
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
// EN: Remove real DbContext
|
|
// VI: Xóa DbContext thật
|
|
var descriptor = services.SingleOrDefault(
|
|
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
|
|
if (descriptor != null)
|
|
services.Remove(descriptor);
|
|
|
|
// EN: Add in-memory database for tests
|
|
// VI: Thêm in-memory database cho tests
|
|
services.AddDbContext<ApplicationDbContext>(options =>
|
|
options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));
|
|
|
|
// EN: Seed test data
|
|
// VI: Seed dữ liệu test
|
|
var sp = services.BuildServiceProvider();
|
|
using var scope = sp.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
db.Database.EnsureCreated();
|
|
SeedTestData(db);
|
|
});
|
|
}
|
|
|
|
private static void SeedTestData(ApplicationDbContext db)
|
|
{
|
|
// Seed any required test data here
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 API Scenario Tests
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Functional tests for Orders API endpoints.
|
|
/// VI: Functional tests cho Orders API endpoints.
|
|
/// </summary>
|
|
public class OrdersApiTests : IClassFixture<CustomWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
|
|
public OrdersApiTests(CustomWebApplicationFactory factory)
|
|
{
|
|
_client = factory.CreateClient();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateOrder_ValidRequest_Returns201WithLocationHeader()
|
|
{
|
|
// Arrange
|
|
var request = new
|
|
{
|
|
UserId = "user-123",
|
|
ShippingAddress = new { Street = "123 St", City = "City", State = "ST", PostalCode = "12345", Country = "VN" },
|
|
Items = new[] { new { ProductId = Guid.NewGuid(), Quantity = 2, UnitPrice = 10.00m } }
|
|
};
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
response.Headers.Location.Should().NotBeNull();
|
|
|
|
var content = await response.Content.ReadFromJsonAsync<OrderCreatedResponse>();
|
|
content!.OrderId.Should().NotBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullOrderWorkflow_CreateUpdateGet_Success()
|
|
{
|
|
// Step 1: Create order
|
|
var createRequest = new
|
|
{
|
|
UserId = "user-workflow",
|
|
ShippingAddress = new { Street = "123 St", City = "City", State = "ST", PostalCode = "12345", Country = "VN" },
|
|
Items = new[] { new { ProductId = Guid.NewGuid(), Quantity = 2, UnitPrice = 10.00m } }
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync("/api/v1/orders", createRequest);
|
|
createResponse.EnsureSuccessStatusCode();
|
|
var created = await createResponse.Content.ReadFromJsonAsync<OrderCreatedResponse>();
|
|
|
|
// Step 2: Get order
|
|
var getResponse = await _client.GetAsync($"/api/v1/orders/{created!.OrderId}");
|
|
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var order = await getResponse.Content.ReadFromJsonAsync<OrderDto>();
|
|
order!.Status.Should().Be("Draft");
|
|
|
|
// Step 3: Submit order
|
|
var submitResponse = await _client.PostAsync(
|
|
$"/api/v1/orders/{created.OrderId}/submit", null);
|
|
submitResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
|
|
// Step 4: Verify status changed
|
|
var verifyResponse = await _client.GetAsync($"/api/v1/orders/{created.OrderId}");
|
|
var updatedOrder = await verifyResponse.Content.ReadFromJsonAsync<OrderDto>();
|
|
updatedOrder!.Status.Should().Be("Submitted");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrder_NotFound_Returns404WithProblemDetails()
|
|
{
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/v1/orders/{Guid.NewGuid()}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
|
problem!.Title.Should().Contain("Not Found");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Giai đoạn 4: Resilience Testing (Khả năng phục hồi)
|
|
|
|
**Mục tiêu**: Đảm bảo hệ thống không sập khi external services gặp lỗi.
|
|
|
|
### 4.1 Testing Retry Policy
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Tests for HTTP retry policy behavior.
|
|
/// VI: Kiểm thử hành vi của retry policy cho HTTP.
|
|
/// </summary>
|
|
public class HttpRetryPolicyTests
|
|
{
|
|
[Fact]
|
|
public async Task RetryPolicy_TransientFailure_RetriesAndSucceeds()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
var mockHandler = new MockHttpMessageHandler(request =>
|
|
{
|
|
callCount++;
|
|
if (callCount < 3)
|
|
return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
|
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
|
});
|
|
|
|
var retryPolicy = Policy<HttpResponseMessage>
|
|
.Handle<HttpRequestException>()
|
|
.OrResult(r => (int)r.StatusCode >= 500)
|
|
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(100));
|
|
|
|
var httpClient = new HttpClient(mockHandler);
|
|
|
|
// Act
|
|
var response = await retryPolicy.ExecuteAsync(() =>
|
|
httpClient.GetAsync("http://test-api/orders"));
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
callCount.Should().Be(3); // 2 failures + 1 success
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Mock HTTP handler for testing.
|
|
/// VI: Mock HTTP handler cho testing.
|
|
/// </summary>
|
|
public class MockHttpMessageHandler : HttpMessageHandler
|
|
{
|
|
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
|
|
|
|
public MockHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
|
{
|
|
_handler = handler;
|
|
}
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(_handler(request));
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.2 Testing Circuit Breaker
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Tests for circuit breaker policy behavior.
|
|
/// VI: Kiểm thử hành vi của circuit breaker policy.
|
|
/// </summary>
|
|
public class CircuitBreakerPolicyTests
|
|
{
|
|
[Fact]
|
|
public async Task CircuitBreaker_ConsecutiveFailures_OpenCircuit()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
var circuitBreaker = Policy
|
|
.Handle<HttpRequestException>()
|
|
.CircuitBreakerAsync(
|
|
exceptionsAllowedBeforeBreaking: 2,
|
|
durationOfBreak: TimeSpan.FromSeconds(30));
|
|
|
|
var failingAction = new Func<Task>(() =>
|
|
{
|
|
callCount++;
|
|
throw new HttpRequestException("Service unavailable");
|
|
});
|
|
|
|
// Act - Trigger failures to open circuit
|
|
await Assert.ThrowsAsync<HttpRequestException>(() =>
|
|
circuitBreaker.ExecuteAsync(failingAction));
|
|
await Assert.ThrowsAsync<HttpRequestException>(() =>
|
|
circuitBreaker.ExecuteAsync(failingAction));
|
|
|
|
// Assert - Circuit should now be open
|
|
circuitBreaker.CircuitState.Should().Be(CircuitState.Open);
|
|
callCount.Should().Be(2);
|
|
|
|
// Further calls should fail immediately with BrokenCircuitException
|
|
await Assert.ThrowsAsync<BrokenCircuitException>(() =>
|
|
circuitBreaker.ExecuteAsync(failingAction));
|
|
callCount.Should().Be(2); // No additional calls made
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Mistakes / Lỗi Thường Gặp
|
|
|
|
### 1. Testing Implementation Details
|
|
|
|
```csharp
|
|
// ❌ BAD: Testing internal state via reflection
|
|
order.GetType().GetField("_status", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
?.GetValue(order).Should().Be(OrderStatus.Draft);
|
|
|
|
// ✅ GOOD: Testing public behavior
|
|
order.Status.Should().Be(OrderStatus.Draft);
|
|
order.Submit();
|
|
order.Status.Should().Be(OrderStatus.Submitted);
|
|
```
|
|
|
|
### 2. Sharing State Between Tests
|
|
|
|
```csharp
|
|
// ❌ BAD: Static shared state causes flaky tests
|
|
private static Order _sharedOrder = new Order(...);
|
|
|
|
// ✅ GOOD: Fresh instance per test (factory method)
|
|
private Order CreateTestOrder() =>
|
|
Order.Create("user-123", CreateAddress());
|
|
```
|
|
|
|
### 3. Ignoring Async Best Practices
|
|
|
|
```csharp
|
|
// ❌ BAD: Blocking call causes deadlocks
|
|
var result = _handler.Handle(command, ct).Result;
|
|
|
|
// ✅ GOOD: Proper async/await
|
|
var result = await _handler.Handle(command, ct);
|
|
```
|
|
|
|
### 4. Missing Cancellation Token Testing
|
|
|
|
```csharp
|
|
// ❌ BAD: Never test cancellation
|
|
await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// ✅ GOOD: Test cancellation handling
|
|
var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
|
_handler.Handle(command, cts.Token));
|
|
```
|
|
|
|
### 5. Overly Specific Mocks
|
|
|
|
```csharp
|
|
// ❌ BAD: Brittle - will break if ID generation changes
|
|
_repo.GetByIdAsync(Arg.Is<Guid>(g => g == new Guid("12345..."))).Returns(order);
|
|
|
|
// ✅ GOOD: Flexible matching
|
|
_repo.GetByIdAsync(Arg.Any<Guid>()).Returns(order);
|
|
```
|
|
|
|
---
|
|
|
|
## Quick Reference / Tham Chiếu Nhanh
|
|
|
|
### Test Attributes
|
|
|
|
| Attribute | Purpose |
|
|
|-----------|---------|
|
|
| `[Fact]` | Single test case |
|
|
| `[Theory]` | Parameterized test |
|
|
| `[InlineData]` | Inline parameters |
|
|
| `[MemberData]` | Complex parameters from method |
|
|
| `[ClassData]` | Complex parameters from class |
|
|
| `[Collection]` | Shared fixture across classes |
|
|
|
|
### NSubstitute Patterns
|
|
|
|
```csharp
|
|
// Create substitute
|
|
var service = Substitute.For<IOrderService>();
|
|
|
|
// Setup return value
|
|
service.GetByIdAsync(Arg.Any<Guid>()).Returns(order);
|
|
|
|
// Setup for any args with condition
|
|
service.GetByIdAsync(Arg.Is<Guid>(id => id != Guid.Empty)).Returns(order);
|
|
|
|
// Verify call count
|
|
await service.Received(1).GetByIdAsync(orderId);
|
|
await service.DidNotReceive().DeleteAsync(Arg.Any<Guid>());
|
|
|
|
// Capture arguments
|
|
Order? capturedOrder = null;
|
|
await repo.AddAsync(Arg.Do<Order>(o => capturedOrder = o));
|
|
```
|
|
|
|
### Test Commands
|
|
|
|
```bash
|
|
# Run all tests
|
|
dotnet test
|
|
|
|
# Run specific project
|
|
dotnet test tests/Service.UnitTests
|
|
|
|
# Run with coverage
|
|
dotnet test --collect:"XPlat Code Coverage"
|
|
|
|
# Run tests matching filter
|
|
dotnet test --filter "FullyQualifiedName~CreateOrder"
|
|
|
|
# Run with detailed output
|
|
dotnet test --logger "console;verbosity=detailed"
|
|
```
|
|
|
|
---
|
|
|
|
## Resources / Tài Nguyên
|
|
|
|
- [Unit Test Rules](./guidelines/unit-test-rules.md) - Quy tắc chi tiết cho Unit Testing
|
|
- [Integration Test Rules](./guidelines/integration-rules.md) - Quy tắc cho Integration Testing
|
|
- [Resilience Test Rules](./guidelines/resilience-test.md) - Quy tắc cho Polly Testing
|
|
- [Full Code Examples](./references/REFERENCE.md) - Ví dụ code đầy đủ
|
|
|
|
### Related Skills
|
|
|
|
- [Testing Patterns](../testing-patterns/SKILL.md) - Complementary testing patterns
|
|
- [Repository Pattern](../repository-pattern/SKILL.md) - Repository testing
|
|
- [Error Handling](../error-handling-patterns/SKILL.md) - Exception testing
|
|
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - Handler testing
|
|
- [API Design](../api-design/SKILL.md) - Controller testing
|