# Full Code Reference / Tham Chiếu Code Đầy Đủ
Tài liệu này chứa các ví dụ code hoàn chỉnh, sẵn sàng sử dụng cho testing .NET Microservices.
## 1. Project Setup / Cài Đặt Project
### 1.1 Test Project Structure
```
tests/
├── ServiceName.UnitTests/
│ ├── ServiceName.UnitTests.csproj
│ ├── GlobalUsings.cs
│ ├── Handlers/
│ │ ├── Commands/
│ │ │ └── CreateOrderCommandHandlerTests.cs
│ │ └── Queries/
│ │ └── GetOrderQueryHandlerTests.cs
│ ├── Domain/
│ │ ├── Entities/
│ │ │ └── OrderTests.cs
│ │ └── ValueObjects/
│ │ └── MoneyTests.cs
│ └── Fixtures/
│ └── TestDataFixture.cs
├── ServiceName.IntegrationTests/
│ ├── ServiceName.IntegrationTests.csproj
│ ├── GlobalUsings.cs
│ ├── Fixtures/
│ │ ├── PostgresFixture.cs
│ │ ├── DatabaseCollection.cs
│ │ └── IntegrationTestFactory.cs
│ ├── Repositories/
│ │ └── OrderRepositoryTests.cs
│ └── Api/
│ └── OrdersApiTests.cs
└── ServiceName.FunctionalTests/
├── ServiceName.FunctionalTests.csproj
└── Scenarios/
└── OrderWorkflowTests.cs
```
### 1.2 Unit Test Project File
```xml
net10.0
enable
enable
false
true
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
```
### 1.3 Integration Test Project File
```xml
net10.0
enable
enable
false
true
all
```
### 1.4 Global Usings
```csharp
// GlobalUsings.cs - Unit Tests
global using Xunit;
global using FluentAssertions;
global using NSubstitute;
global using NSubstitute.ExceptionExtensions;
```
```csharp
// GlobalUsings.cs - Integration Tests
global using Xunit;
global using FluentAssertions;
global using NSubstitute;
global using Microsoft.AspNetCore.Mvc.Testing;
global using Microsoft.EntityFrameworkCore;
global using Testcontainers.PostgreSql;
global using System.Net;
global using System.Net.Http.Json;
```
---
## 2. Complete Test Examples / Ví Dụ Test Hoàn Chỉnh
### 2.1 Domain Entity Tests
```csharp
///
/// EN: Comprehensive tests for Order aggregate root.
/// VI: Kiểm thử toàn diện cho Order aggregate root.
///
namespace ServiceName.UnitTests.Domain.Entities;
public class OrderTests
{
#region Creation Tests
[Fact]
public void Create_ValidParameters_ReturnsOrderWithDraftStatus()
{
// Arrange
var userId = "user-123";
var address = CreateTestAddress();
// Act
var order = Order.Create(userId, address);
// Assert
order.Should().NotBeNull();
order.Id.Should().NotBeEmpty();
order.UserId.Should().Be(userId);
order.Status.Should().Be(OrderStatus.Draft);
order.ShippingAddress.Should().Be(address);
order.OrderItems.Should().BeEmpty();
order.TotalAmount.Should().Be(0m);
order.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Create_InvalidUserId_ThrowsDomainException(string? userId)
{
// Arrange
var address = CreateTestAddress();
// Act
var act = () => Order.Create(userId!, address);
// Assert
act.Should().Throw()
.WithMessage("*user*required*");
}
[Fact]
public void Create_NullAddress_ThrowsDomainException()
{
// Act
var act = () => Order.Create("user-123", null!);
// Assert
act.Should().Throw()
.WithMessage("*address*required*");
}
#endregion
#region AddItem Tests
[Fact]
public void AddItem_ValidItem_AddsToOrderItems()
{
// Arrange
var order = CreateTestOrder();
var productId = Guid.NewGuid();
// Act
order.AddItem(productId, quantity: 2, unitPrice: 10.00m);
// Assert
order.OrderItems.Should().HaveCount(1);
order.OrderItems.First().ProductId.Should().Be(productId);
order.OrderItems.First().Quantity.Should().Be(2);
order.OrderItems.First().UnitPrice.Should().Be(10.00m);
}
[Fact]
public void AddItem_ValidItem_UpdatesTotalAmount()
{
// Arrange
var order = CreateTestOrder();
// Act
order.AddItem(Guid.NewGuid(), quantity: 2, unitPrice: 10.00m);
order.AddItem(Guid.NewGuid(), quantity: 1, unitPrice: 25.00m);
// Assert
order.TotalAmount.Should().Be(45.00m);
}
[Fact]
public void AddItem_SameProduct_IncreasesQuantityInsteadOfDuplicate()
{
// Arrange
var order = CreateTestOrder();
var productId = Guid.NewGuid();
// Act
order.AddItem(productId, quantity: 2, unitPrice: 10.00m);
order.AddItem(productId, quantity: 3, unitPrice: 10.00m);
// Assert
order.OrderItems.Should().HaveCount(1);
order.OrderItems.First().Quantity.Should().Be(5);
order.TotalAmount.Should().Be(50.00m);
}
[Fact]
public void AddItem_RaisesDomainEvent()
{
// Arrange
var order = CreateTestOrder();
// Act
order.AddItem(Guid.NewGuid(), 2, 10.00m);
// Assert
order.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType();
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void AddItem_InvalidQuantity_ThrowsDomainException(int quantity)
{
// Arrange
var order = CreateTestOrder();
// Act
var act = () => order.AddItem(Guid.NewGuid(), quantity, 10.00m);
// Assert
act.Should().Throw()
.WithMessage("*quantity*positive*");
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-0.01)]
public void AddItem_InvalidUnitPrice_ThrowsDomainException(decimal unitPrice)
{
// Arrange
var order = CreateTestOrder();
// Act
var act = () => order.AddItem(Guid.NewGuid(), 1, unitPrice);
// Assert
act.Should().Throw()
.WithMessage("*price*positive*");
}
[Fact]
public void AddItem_SubmittedOrder_ThrowsDomainException()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 1, 10.00m);
order.Submit();
// Act
var act = () => order.AddItem(Guid.NewGuid(), 1, 20.00m);
// Assert
act.Should().Throw()
.WithMessage("*cannot modify*submitted*");
}
#endregion
#region RemoveItem Tests
[Fact]
public void RemoveItem_ExistingItem_RemovesFromOrder()
{
// Arrange
var order = CreateTestOrder();
var productId = Guid.NewGuid();
order.AddItem(productId, 2, 10.00m);
// Act
order.RemoveItem(productId);
// Assert
order.OrderItems.Should().BeEmpty();
order.TotalAmount.Should().Be(0m);
}
[Fact]
public void RemoveItem_NonExistingItem_ThrowsDomainException()
{
// Arrange
var order = CreateTestOrder();
// Act
var act = () => order.RemoveItem(Guid.NewGuid());
// Assert
act.Should().Throw()
.WithMessage("*item*not found*");
}
#endregion
#region Submit Tests
[Fact]
public void Submit_ValidDraftOrder_ChangesStatusToSubmitted()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 2, 10.00m);
// Act
order.Submit();
// Assert
order.Status.Should().Be(OrderStatus.Submitted);
order.SubmittedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public void Submit_RaisesOrderSubmittedEvent()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 2, 10.00m);
order.ClearDomainEvents(); // Clear AddItem event
// Act
order.Submit();
// Assert
order.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType();
}
[Fact]
public void Submit_EmptyOrder_ThrowsDomainException()
{
// Arrange
var order = CreateTestOrder();
// Act
var act = () => order.Submit();
// Assert
act.Should().Throw()
.WithMessage("*cannot submit*empty*");
}
[Fact]
public void Submit_AlreadySubmitted_ThrowsDomainException()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 2, 10.00m);
order.Submit();
// Act
var act = () => order.Submit();
// Assert
act.Should().Throw()
.WithMessage("*already submitted*");
}
#endregion
#region Cancel Tests
[Fact]
public void Cancel_DraftOrder_ChangesStatusToCancelled()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 2, 10.00m);
// Act
order.Cancel("Changed my mind");
// Assert
order.Status.Should().Be(OrderStatus.Cancelled);
order.CancellationReason.Should().Be("Changed my mind");
}
[Fact]
public void Cancel_CompletedOrder_ThrowsDomainException()
{
// Arrange
var order = CreateTestOrder();
order.AddItem(Guid.NewGuid(), 2, 10.00m);
order.Submit();
order.Complete();
// Act
var act = () => order.Cancel("Too late");
// Assert
act.Should().Throw()
.WithMessage("*cannot cancel*completed*");
}
#endregion
#region Helper Methods
private static Order CreateTestOrder() =>
Order.Create("user-123", CreateTestAddress());
private static Address CreateTestAddress() =>
Address.Create("123 Main St", "Ho Chi Minh City", "HCM", "70000", "VN");
#endregion
}
```
### 2.2 Value Object Tests
```csharp
///
/// EN: Comprehensive tests for Money value object.
/// VI: Kiểm thử toàn diện cho Money value object.
///
namespace ServiceName.UnitTests.Domain.ValueObjects;
public class MoneyTests
{
#region Creation Tests
[Fact]
public void Create_ValidParameters_ReturnsMoney()
{
// Act
var money = Money.Create(100.50m, "VND");
// Assert
money.Amount.Should().Be(100.50m);
money.Currency.Should().Be("VND");
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100.50)]
public void Create_NonPositiveAmount_ThrowsException(decimal amount)
{
// Act
var act = () => Money.Create(amount, "VND");
// Assert
act.Should().Throw()
.WithMessage("*amount*positive*");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Create_InvalidCurrency_ThrowsException(string? currency)
{
// Act
var act = () => Money.Create(100, currency!);
// Assert
act.Should().Throw()
.WithMessage("*currency*required*");
}
#endregion
#region Arithmetic Tests
[Theory]
[InlineData(100, 50, 150)]
[InlineData(100.50, 25.25, 125.75)]
[InlineData(0.01, 0.01, 0.02)]
public void Add_SameCurrency_ReturnsSum(decimal a, decimal b, decimal expected)
{
// Arrange
var money1 = Money.Create(a, "VND");
var money2 = Money.Create(b, "VND");
// Act
var result = money1.Add(money2);
// Assert
result.Amount.Should().Be(expected);
result.Currency.Should().Be("VND");
}
[Fact]
public void Add_DifferentCurrency_ThrowsException()
{
// Arrange
var vnd = Money.Create(100, "VND");
var usd = Money.Create(10, "USD");
// Act
var act = () => vnd.Add(usd);
// Assert
act.Should().Throw()
.WithMessage("*currency mismatch*");
}
[Fact]
public void Subtract_SameCurrency_ReturnsDifference()
{
// Arrange
var money1 = Money.Create(100, "VND");
var money2 = Money.Create(30, "VND");
// Act
var result = money1.Subtract(money2);
// Assert
result.Amount.Should().Be(70);
}
[Fact]
public void Subtract_ResultNegative_ThrowsException()
{
// Arrange
var money1 = Money.Create(30, "VND");
var money2 = Money.Create(100, "VND");
// Act
var act = () => money1.Subtract(money2);
// Assert
act.Should().Throw()
.WithMessage("*negative*");
}
[Theory]
[InlineData(100, 2, 200)]
[InlineData(50.25, 3, 150.75)]
public void Multiply_ValidMultiplier_ReturnsProduct(decimal amount, int multiplier, decimal expected)
{
// Arrange
var money = Money.Create(amount, "VND");
// Act
var result = money.Multiply(multiplier);
// Assert
result.Amount.Should().Be(expected);
}
#endregion
#region Equality Tests
[Fact]
public void Equals_SameAmountAndCurrency_ReturnsTrue()
{
// Arrange
var money1 = Money.Create(100, "VND");
var money2 = Money.Create(100, "VND");
// Assert
money1.Should().Be(money2);
(money1 == money2).Should().BeTrue();
money1.GetHashCode().Should().Be(money2.GetHashCode());
}
[Fact]
public void Equals_DifferentAmount_ReturnsFalse()
{
// Arrange
var money1 = Money.Create(100, "VND");
var money2 = Money.Create(200, "VND");
// Assert
money1.Should().NotBe(money2);
(money1 != money2).Should().BeTrue();
}
[Fact]
public void Equals_DifferentCurrency_ReturnsFalse()
{
// Arrange
var money1 = Money.Create(100, "VND");
var money2 = Money.Create(100, "USD");
// Assert
money1.Should().NotBe(money2);
}
#endregion
#region Immutability Tests
[Fact]
public void Add_ReturnsNewInstance_DoesNotModifyOriginal()
{
// Arrange
var original = Money.Create(100, "VND");
var toAdd = Money.Create(50, "VND");
// Act
var result = original.Add(toAdd);
// Assert
result.Should().NotBeSameAs(original);
original.Amount.Should().Be(100); // Unchanged
result.Amount.Should().Be(150);
}
#endregion
}
```
### 2.3 Command Handler Tests
```csharp
///
/// EN: Comprehensive tests for CreateOrderCommandHandler.
/// VI: Kiểm thử toàn diện cho CreateOrderCommandHandler.
///
namespace ServiceName.UnitTests.Handlers.Commands;
public class CreateOrderCommandHandlerTests
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger _logger;
private readonly IPublisher _publisher;
private readonly CreateOrderCommandHandler _handler;
public CreateOrderCommandHandlerTests()
{
_orderRepository = Substitute.For();
_logger = Substitute.For>();
_publisher = Substitute.For();
// Setup UnitOfWork mock
var unitOfWork = Substitute.For();
unitOfWork.SaveChangesAsync(Arg.Any()).Returns(1);
_orderRepository.UnitOfWork.Returns(unitOfWork);
_handler = new CreateOrderCommandHandler(
_orderRepository,
_publisher,
_logger);
}
[Fact]
public async Task Handle_ValidCommand_CreatesOrderAndReturnsResult()
{
// Arrange
var command = CreateValidCommand();
_orderRepository.AddAsync(Arg.Any(), Arg.Any())
.Returns(callInfo => callInfo.Arg());
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.OrderId.Should().NotBeEmpty();
result.Status.Should().Be("Draft");
}
[Fact]
public async Task Handle_ValidCommand_PersistsOrderToRepository()
{
// Arrange
var command = CreateValidCommand();
Order? capturedOrder = null;
_orderRepository.AddAsync(Arg.Do(o => capturedOrder = o), Arg.Any())
.Returns(callInfo => callInfo.Arg());
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
await _orderRepository.Received(1).AddAsync(Arg.Any(), Arg.Any());
await _orderRepository.UnitOfWork.Received(1).SaveChangesAsync(Arg.Any());
capturedOrder.Should().NotBeNull();
capturedOrder!.UserId.Should().Be(command.UserId);
capturedOrder.OrderItems.Should().HaveCount(command.Items.Count);
}
[Fact]
public async Task Handle_ValidCommand_PublishesDomainEvents()
{
// Arrange
var command = CreateValidCommand();
_orderRepository.AddAsync(Arg.Any(), Arg.Any())
.Returns(callInfo => callInfo.Arg());
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
await _publisher.Received().Publish(
Arg.Is(e => e.ProductId == command.Items[0].ProductId),
Arg.Any());
}
[Fact]
public async Task Handle_EmptyItems_ThrowsValidationException()
{
// Arrange
var command = new CreateOrderCommand(
UserId: "user-123",
ShippingAddress: CreateAddressDto(),
Items: new List());
// Act & Assert
var act = async () => await _handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync()
.WithMessage("*items*required*");
await _orderRepository.DidNotReceive().AddAsync(Arg.Any(), Arg.Any());
}
[Fact]
public async Task Handle_NullUserId_ThrowsValidationException()
{
// Arrange
var command = new CreateOrderCommand(
UserId: null!,
ShippingAddress: CreateAddressDto(),
Items: new List
{
new(Guid.NewGuid(), 1, 10.00m)
});
// Act & Assert
var act = async () => await _handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync()
.WithMessage("*user*required*");
}
[Fact]
public async Task Handle_RepositoryThrows_PropagatesException()
{
// Arrange
var command = CreateValidCommand();
_orderRepository.AddAsync(Arg.Any(), Arg.Any())
.ThrowsAsync(new DbUpdateException("Database error"));
// Act & Assert
var act = async () => await _handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync();
}
[Fact]
public async Task Handle_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
var command = CreateValidCommand();
var cts = new CancellationTokenSource();
cts.Cancel();
_orderRepository.AddAsync(Arg.Any(), Arg.Any())
.Returns(async callInfo =>
{
var ct = callInfo.Arg();
ct.ThrowIfCancellationRequested();
return callInfo.Arg();
});
// Act & Assert
var act = async () => await _handler.Handle(command, cts.Token);
await act.Should().ThrowAsync();
}
#region Helper Methods
private static CreateOrderCommand CreateValidCommand() => new(
UserId: "user-123",
ShippingAddress: CreateAddressDto(),
Items: new List
{
new(ProductId: Guid.NewGuid(), Quantity: 2, UnitPrice: 10.00m),
new(ProductId: Guid.NewGuid(), Quantity: 1, UnitPrice: 25.00m)
});
private static AddressDto CreateAddressDto() => new(
Street: "123 Main St",
City: "Ho Chi Minh City",
State: "HCM",
PostalCode: "70000",
Country: "VN");
#endregion
}
```
---
## 3. Integration Test Examples
### 3.1 Complete Integration Test Suite
```csharp
///
/// EN: Complete integration test suite for Orders API.
/// VI: Bộ integration test hoàn chỉnh cho Orders API.
///
namespace ServiceName.IntegrationTests.Api;
[Collection("Database")]
public class OrdersApiIntegrationTests : IClassFixture
{
private readonly HttpClient _client;
private readonly IntegrationTestFactory _factory;
public OrdersApiIntegrationTests(IntegrationTestFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
}
#region Create Order Tests
[Fact]
public async Task CreateOrder_ValidRequest_Returns201Created()
{
// Arrange
var request = CreateValidRequest();
// 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();
content!.OrderId.Should().NotBeEmpty();
}
[Fact]
public async Task CreateOrder_ValidRequest_PersistsToDatabase()
{
// Arrange
var request = CreateValidRequest();
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
var content = await response.Content.ReadFromJsonAsync();
// Assert - Verify database persistence
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var order = await db.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == content!.OrderId);
order.Should().NotBeNull();
order!.OrderItems.Should().HaveCount(request.Items.Length);
}
[Fact]
public async Task CreateOrder_EmptyItems_Returns400BadRequest()
{
// Arrange
var request = new
{
ShippingAddress = CreateAddressDto(),
Items = Array.Empty