Nhiều team viết test vì "phải có coverage". Kết quả: test brittle, test chậm, test sai thứ. Session này giúp bạn test ĐÚNG THỨ, đúng mức.
Mục tiêu
- Hiểu Test Pyramid và áp dụng thực tế
- Biết test gì và KHÔNG test gì
- Unit vs Integration: khi nào dùng gì
- Test patterns cho .NET
1) Test Pyramid - Thực Tế
┌──────┐
│ E2E │ Ít nhất, chậm nhất, brittle nhất
│ │ → Chỉ happy path + critical paths
┌┴──────┴┐
│ Integ- │ Trung bình, test component interactions
│ ration │ → API endpoints, DB queries, message handlers
┌┴────────┴┐
│ Unit │ Nhiều nhất, nhanh nhất, stable nhất
│ │ → Business logic, validation, calculations
└───────────┘
Thực tế .NET project
Unit Tests:
- Domain models (aggregate, value objects)
- Validators (FluentValidation)
- Pure functions (calculations, transformations)
- MediatR handlers (mock dependencies)
→ Nhanh (<1ms per test), nhiều tests
Integration Tests:
- API endpoints (WebApplicationFactory)
- Database operations (EF Core + TestContainers)
- Message consumers (MassTransit test harness)
→ Chậm hơn (100ms-2s), ít tests hơn
E2E Tests:
- Critical user flows (đặt hàng, thanh toán)
- UI testing (Playwright)
→ Chậm nhất (5-30s), ít nhất, dễ brittle
2) Test GÌ - Test KHÔNG Gì
SHOULD Test
csharp// ✅ Business logic (CÓ GIÁ TRỊ CAO)
[Fact]
public void Order_ApplyDiscount_ShouldReduceTotal()
{
var order = Order.Create(customerId, items);
order.ApplyDiscount(Discount.Percentage(10));
Assert.Equal(90m, order.Total.Amount);
}
// ✅ Edge cases (BUGS ẩn ở đây)
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(int.MaxValue)]
public void Order_AddItem_InvalidQuantity_ShouldFail(int quantity)
{
var order = Order.Create(customerId);
var result = order.AddItem(productId, quantity);
Assert.True(result.IsFailure);
}
// ✅ Validation rules (boundary protection)
[Fact]
public void CreateOrderValidator_EmptyItems_ShouldFail()
{
var validator = new CreateOrderValidator();
var result = validator.Validate(new CreateOrderCommand { Items = [] });
Assert.False(result.IsValid);
}
SHOULD NOT Test
csharp// ❌ Framework code (EF Core, ASP.NET đã test rồi)
[Fact]
public void DbContext_CanSaveAndRetrieve()
{
db.Users.Add(new User { Name = "test" });
db.SaveChanges();
var user = db.Users.First();
Assert.Equal("test", user.Name);
}
// → Test EF Core hoạt động? Microsoft đã test rồi!
// ❌ Trivial code
[Fact]
public void User_Name_ShouldReturnName()
{
var user = new User { Name = "John" };
Assert.Equal("John", user.Name); // 🤡 Test getter?
}
// ❌ Private methods (test qua public interface)
// ❌ Configuration (test integration, không unit)
// ❌ Third-party library internals
3) Unit Test - Focus Business Logic
csharp// Pattern: Arrange → Act → Assert
[Fact]
public void Order_Submit_WithInsufficientStock_ShouldFail()
{
// Arrange
var order = OrderFactory.CreateDraft();
var product = ProductFactory.CreateWithStock(5);
order.AddItem(product.Id, quantity: 10); // Muốn mua 10, chỉ có 5
// Act
var result = order.Submit(stockChecker: new FakeStockChecker(product));
// Assert
Assert.True(result.IsFailure);
Assert.Equal("Insufficient stock", result.Error);
Assert.Equal(OrderStatus.Draft, order.Status); // Vẫn Draft, KHÔNG Submit
}
// Mock dependencies: chỉ mock EXTERNAL dependencies (DB, HTTP, Email)
// KHÔNG mock domain logic!
[Fact]
public async Task CreateOrderHandler_ShouldSaveAndPublishEvent()
{
// Arrange
var repo = new Mock<IOrderRepository>();
var uow = new Mock<IUnitOfWork>();
var handler = new CreateOrderHandler(repo.Object, uow.Object);
// Act
var result = await handler.Handle(new CreateOrderCommand { /* ... */ }, CancellationToken.None);
// Assert
Assert.True(result.IsSuccess);
repo.Verify(r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once);
uow.Verify(u => u.CommitAsync(It.IsAny<CancellationToken>()), Times.Once);
}
4) Integration Test - Test Real Dependencies
csharp// WebApplicationFactory: spin up real ASP.NET Core app in test
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrdersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
// Replace real DB with test DB
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
// Replace real email with fake
services.RemoveAll<IEmailService>();
services.AddSingleton<IEmailService, FakeEmailService>();
});
}).CreateClient();
}
[Fact]
public async Task CreateOrder_ValidRequest_Returns201()
{
// Arrange
var request = new { ProductId = Guid.NewGuid(), Quantity = 2 };
// Act
var response = await _client.PostAsJsonAsync("/api/orders", request);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var order = await response.Content.ReadFromJsonAsync<OrderDto>();
Assert.NotNull(order);
Assert.NotEqual(Guid.Empty, order.Id);
}
[Fact]
public async Task CreateOrder_Unauthorized_Returns401()
{
// No auth token
var response = await _client.PostAsJsonAsync("/api/orders", new { });
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
TestContainers (real database in Docker)
csharp// Thay InMemoryDatabase bằng real SQL/PostgreSQL
public class OrdersApiTests : IAsyncLifetime
{
private readonly MsSqlContainer _dbContainer = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
public async Task InitializeAsync() => await _dbContainer.StartAsync();
public async Task DisposeAsync() => await _dbContainer.DisposeAsync();
// Tests dùng _dbContainer.GetConnectionString() → real SQL queries!
}
5) Test Naming & Organization
csharp// Naming: MethodName_Scenario_ExpectedResult
[Fact]
public void Order_AddItem_NegativeQuantity_ShouldReturnFailure() { }
// Hoặc: GivenWhenThen
[Fact]
public void GivenEmptyOrder_WhenSubmitting_ThenShouldFail() { }
// File organization:
// Tests/
// Unit/
// Domain/
// OrderTests.cs
// MoneyTests.cs
// Validators/
// CreateOrderValidatorTests.cs
// Integration/
// Api/
// OrdersApiTests.cs
// Repositories/
// OrderRepositoryTests.cs
Master Checklist ✅
□ Business logic có unit tests?
□ API endpoints có integration tests (happy + error paths)?
□ Validation rules tested (boundary values)?
□ Tests KHÔNG test framework internals?
□ Mock chỉ external dependencies (DB, HTTP)?
□ Tests chạy nhanh? (unit <10s, integration <60s)
□ Tests deterministic? (no random, no datetime.now)
□ Critical paths có E2E tests?
Tư duy master: Test tốt nhất là test mà khi FAIL, bạn biết NGAY bug ở đâu. Nếu test fail mà bạn phải debug 30 phút mới biết lỗi → test quá coarse hoặc sai level.