# Testing Patterns - Detailed Reference

Detailed code examples for testing patterns in ASP.NET Core with xUnit and NSubstitute.

## Table of Contents

1. [Project Setup](#project-setup)
2. [Unit Testing MediatR Handlers](#unit-testing-mediatr-handlers)
3. [Domain Entity Testing](#domain-entity-testing)
4. [Controller Testing](#controller-testing)
5. [Integration Testing with Testcontainers](#integration-testing-with-testcontainers)
6. [Functional Testing with TestServer](#functional-testing-with-testserver)
7. [Testing Validation](#testing-validation)
8. [Test Data Builders](#test-data-builders)

---

## Project Setup

### Test Project Configuration

```xml
<!-- ServiceName.UnitTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.6.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
    <PackageReference Include="NSubstitute" Version="5.1.0" />
    <PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.16" />
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
    <PackageReference Include="coverlet.collector" Version="6.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\ServiceName.API\ServiceName.API.csproj" />
  </ItemGroup>
</Project>
```

### Integration Test Project

```xml
<!-- ServiceName.IntegrationTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.6.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
    <PackageReference Include="Testcontainers.PostgreSql" Version="3.6.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\ServiceName.API\ServiceName.API.csproj" />
    <ProjectReference Include="..\..\src\ServiceName.Infrastructure\ServiceName.Infrastructure.csproj" />
  </ItemGroup>
</Project>
```

---

## Unit Testing MediatR Handlers

### Command Handler Tests

```csharp
/// <summary>
/// EN: Tests for CreateOrderCommandHandler.
/// VI: Tests cho CreateOrderCommandHandler.
/// </summary>
public class CreateOrderCommandHandlerTests
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUserService _userService;
    private readonly ILogger<CreateOrderCommandHandler> _logger;
    private readonly CreateOrderCommandHandler _handler;

    public CreateOrderCommandHandlerTests()
    {
        _orderRepository = Substitute.For<IOrderRepository>();
        _userService = Substitute.For<IUserService>();
        _logger = Substitute.For<ILogger<CreateOrderCommandHandler>>();

        // EN: Setup default returns
        // VI: Thiết lập giá trị trả về mặc định
        _orderRepository.UnitOfWork.SaveChangesAsync(Arg.Any<CancellationToken>())
            .Returns(1);

        _handler = new CreateOrderCommandHandler(
            _orderRepository,
            _userService,
            _logger);
    }

    [Fact]
    public async Task Handle_ValidCommand_CreatesOrderSuccessfully()
    {
        // Arrange
        var command = CreateValidCommand();
        Order? capturedOrder = null;

        _orderRepository.AddAsync(Arg.Do<Order>(o => capturedOrder = o), Arg.Any<CancellationToken>())
            .Returns(callInfo => callInfo.Arg<Order>());

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.OrderId.Should().NotBeEmpty();

        capturedOrder.Should().NotBeNull();
        capturedOrder!.UserId.Should().Be(command.UserId);
        capturedOrder.OrderItems.Should().HaveCount(command.Items.Count);

        await _orderRepository.Received(1).AddAsync(
            Arg.Any<Order>(),
            Arg.Any<CancellationToken>());
        await _orderRepository.UnitOfWork.Received(1)
            .SaveChangesAsync(Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_EmptyItems_ThrowsDomainException()
    {
        // Arrange
        var command = CreateValidCommand() with { Items = new List<OrderItemDto>() };

        // Act
        var act = () => _handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<DomainException>()
            .WithMessage("*empty*");

        await _orderRepository.DidNotReceive().AddAsync(
            Arg.Any<Order>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_RepositoryThrows_LogsAndRethrows()
    {
        // Arrange
        var command = CreateValidCommand();
        var expectedException = new InvalidOperationException("Database error");

        _orderRepository.AddAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(expectedException);

        // Act
        var act = () => _handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<InvalidOperationException>()
            .WithMessage("Database error");
    }

    private static CreateOrderCommand CreateValidCommand() =>
        new(
            UserId: "user-123",
            ShippingAddress: new Address("123 Main St", "City", "State", "12345", "US"),
            Items: new List<OrderItemDto>
            {
                new(Guid.NewGuid(), 2, 10.00m),
                new(Guid.NewGuid(), 1, 25.00m)
            });
}
```

### Query Handler Tests

```csharp
/// <summary>
/// EN: Tests for GetOrderQueryHandler.
/// VI: Tests cho GetOrderQueryHandler.
/// </summary>
public class GetOrderQueryHandlerTests
{
    private readonly IOrderRepository _orderRepository;
    private readonly GetOrderQueryHandler _handler;

    public GetOrderQueryHandlerTests()
    {
        _orderRepository = Substitute.For<IOrderRepository>();
        _handler = new GetOrderQueryHandler(_orderRepository);
    }

    [Fact]
    public async Task Handle_OrderExists_ReturnsOrderDto()
    {
        // Arrange
        var orderId = Guid.NewGuid();
        var order = CreateOrder(orderId, "user-123");

        _orderRepository.GetWithItemsAsync(orderId, Arg.Any<CancellationToken>())
            .Returns(order);

        var query = new GetOrderQuery(orderId, "user-123");

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(orderId);
        result.UserId.Should().Be("user-123");
    }

    [Fact]
    public async Task Handle_OrderNotFound_ReturnsNull()
    {
        // Arrange
        var query = new GetOrderQuery(Guid.NewGuid(), "user-123");

        _orderRepository.GetWithItemsAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
            .Returns((Order?)null);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task Handle_DifferentUser_ReturnsNull()
    {
        // Arrange
        var orderId = Guid.NewGuid();
        var order = CreateOrder(orderId, "user-123");

        _orderRepository.GetWithItemsAsync(orderId, Arg.Any<CancellationToken>())
            .Returns(order);

        // EN: Query with different user
        // VI: Query với user khác
        var query = new GetOrderQuery(orderId, "different-user");

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().BeNull();
    }

    private static Order CreateOrder(Guid id, string userId)
    {
        var order = new Order(userId, new Address("St", "City", "ST", "12345", "US"));
        // EN: Use reflection to set Id for testing
        // VI: Dùng reflection để set Id cho testing
        typeof(Order).GetProperty("Id")!.SetValue(order, id);
        return order;
    }
}
```

---

## Domain Entity Testing

### Aggregate Root Tests

```csharp
/// <summary>
/// EN: Tests for Order aggregate root behavior.
/// VI: Tests cho behavior của Order aggregate root.
/// </summary>
public class OrderTests
{
    [Fact]
    public void Constructor_ValidParameters_CreatesOrder()
    {
        // Act
        var order = new Order("user-123", CreateAddress());

        // Assert
        order.Id.Should().NotBeEmpty();
        order.UserId.Should().Be("user-123");
        order.Status.Should().Be(OrderStatus.Draft);
        order.TotalAmount.Should().Be(0);
        order.OrderItems.Should().BeEmpty();
    }

    [Fact]
    public void AddItem_NewProduct_AddsToItems()
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());
        var productId = Guid.NewGuid();

        // Act
        order.AddItem(productId, quantity: 2, unitPrice: 10.00m);

        // Assert
        order.OrderItems.Should().HaveCount(1);
        var item = order.OrderItems.First();
        item.ProductId.Should().Be(productId);
        item.Quantity.Should().Be(2);
        item.UnitPrice.Should().Be(10.00m);
        order.TotalAmount.Should().Be(20.00m);
    }

    [Fact]
    public void AddItem_ExistingProduct_IncreasesQuantity()
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());
        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_SubmittedOrder_ThrowsDomainException()
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());
        order.AddItem(Guid.NewGuid(), 1, 10.00m);
        order.Submit();

        // Act
        var act = () => order.AddItem(Guid.NewGuid(), 1, 10.00m);

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Cannot add items to non-draft order");
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(-100)]
    public void AddItem_InvalidQuantity_ThrowsArgumentException(int quantity)
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());

        // Act
        var act = () => order.AddItem(Guid.NewGuid(), quantity, 10.00m);

        // Assert
        act.Should().Throw<ArgumentException>()
            .WithMessage("*positive*");
    }

    [Fact]
    public void Submit_ValidOrder_ChangesStatus()
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());
        order.AddItem(Guid.NewGuid(), 1, 10.00m);

        // Act
        order.Submit();

        // Assert
        order.Status.Should().Be(OrderStatus.Submitted);
    }

    [Fact]
    public void Submit_EmptyOrder_ThrowsDomainException()
    {
        // Arrange
        var order = new Order("user-123", CreateAddress());

        // Act
        var act = () => order.Submit();

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Cannot submit empty order");
    }

    private static Address CreateAddress() =>
        new("123 Main St", "City", "State", "12345", "Country");
}
```

---

## Controller Testing

### Controller Unit Tests

```csharp
/// <summary>
/// EN: Tests for OrdersController.
/// VI: Tests cho OrdersController.
/// </summary>
public class OrdersControllerTests
{
    private readonly IMediator _mediator;
    private readonly OrdersController _controller;

    public OrdersControllerTests()
    {
        _mediator = Substitute.For<IMediator>();
        _controller = new OrdersController(_mediator);

        // EN: Setup mock user claims
        // VI: Thiết lập user claims giả lập
        var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.NameIdentifier, "user-123")
        }, "TestAuth"));

        _controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext { User = user }
        };
    }

    [Fact]
    public async Task CreateOrder_ValidRequest_ReturnsCreated()
    {
        // Arrange
        var request = new CreateOrderRequest(
            new AddressDto("St", "City", "ST", "12345", "US"),
            new[] { new OrderItemDto(Guid.NewGuid(), 2, 10.00m) });

        var expectedResult = new OrderResult(Guid.NewGuid());

        _mediator.Send(Arg.Any<CreateOrderCommand>(), Arg.Any<CancellationToken>())
            .Returns(expectedResult);

        // Act
        var result = await _controller.CreateOrder(request, CancellationToken.None);

        // Assert
        var createdResult = result.Result.Should().BeOfType<CreatedAtActionResult>().Subject;
        var response = createdResult.Value.Should().BeOfType<ApiResponse<OrderResult>>().Subject;
        response.Success.Should().BeTrue();
        response.Data.Should().Be(expectedResult);
    }

    [Fact]
    public async Task GetOrder_NotFound_ReturnsNotFound()
    {
        // Arrange
        var orderId = Guid.NewGuid();

        _mediator.Send(Arg.Any<GetOrderQuery>(), Arg.Any<CancellationToken>())
            .Returns((OrderDto?)null);

        // Act
        var result = await _controller.GetOrder(orderId, CancellationToken.None);

        // Assert
        result.Result.Should().BeOfType<NotFoundObjectResult>();
    }

    [Fact]
    public async Task GetOrder_Exists_ReturnsOk()
    {
        // Arrange
        var orderId = Guid.NewGuid();
        var orderDto = new OrderDto(orderId, "user-123", "Draft", 100m, DateTime.UtcNow);

        _mediator.Send(Arg.Any<GetOrderQuery>(), Arg.Any<CancellationToken>())
            .Returns(orderDto);

        // Act
        var result = await _controller.GetOrder(orderId, CancellationToken.None);

        // Assert
        var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
        var response = okResult.Value.Should().BeOfType<ApiResponse<OrderDto>>().Subject;
        response.Success.Should().BeTrue();
        response.Data!.Id.Should().Be(orderId);
    }
}
```

---

## Integration Testing with Testcontainers

### Database Fixture

```csharp
/// <summary>
/// EN: Shared database fixture for integration tests.
/// VI: Fixture database dùng chung cho integration tests.
/// </summary>
public class DatabaseFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
        .WithImage("postgres:15-alpine")
        .WithDatabase("testdb")
        .WithUsername("test")
        .WithPassword("test")
        .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: Create fresh DbContext for each test.
    /// VI: Tạo DbContext mới cho mỗi test.
    /// </summary>
    public ApplicationDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(ConnectionString)
            .Options;
        return new ApplicationDbContext(options);
    }
}

[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
```

### Repository Integration Tests

```csharp
/// <summary>
/// EN: Integration tests for OrderRepository.
/// VI: Integration tests cho OrderRepository.
/// </summary>
[Collection("Database")]
public class OrderRepositoryIntegrationTests : IAsyncLifetime
{
    private readonly DatabaseFixture _fixture;
    private ApplicationDbContext _context = null!;
    private OrderRepository _repository = null!;

    public OrderRepositoryIntegrationTests(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }

    public Task InitializeAsync()
    {
        _context = _fixture.CreateContext();
        _repository = new OrderRepository(_context);
        return Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        // EN: Clean up test data
        // VI: Dọn dẹp dữ liệu test
        _context.Orders.RemoveRange(_context.Orders);
        await _context.SaveChangesAsync();
        await _context.DisposeAsync();
    }

    [Fact]
    public async Task AddAsync_ValidOrder_PersistsToDatabase()
    {
        // Arrange
        var order = new Order("user-123", new Address("St", "City", "ST", "12345", "US"));
        order.AddItem(Guid.NewGuid(), 2, 10.00m);

        // Act
        var addedOrder = await _repository.AddAsync(order);
        await _repository.UnitOfWork.SaveChangesAsync();

        // Assert - verify with fresh context
        using var verifyContext = _fixture.CreateContext();
        var savedOrder = await verifyContext.Orders
            .Include(o => o.OrderItems)
            .FirstOrDefaultAsync(o => o.Id == addedOrder.Id);

        savedOrder.Should().NotBeNull();
        savedOrder!.UserId.Should().Be("user-123");
        savedOrder.OrderItems.Should().HaveCount(1);
        savedOrder.TotalAmount.Should().Be(20.00m);
    }

    [Fact]
    public async Task GetWithItemsAsync_OrderExists_IncludesItems()
    {
        // Arrange
        var order = new Order("user-123", new Address("St", "City", "ST", "12345", "US"));
        order.AddItem(Guid.NewGuid(), 2, 10.00m);
        order.AddItem(Guid.NewGuid(), 1, 25.00m);

        await _repository.AddAsync(order);
        await _repository.UnitOfWork.SaveChangesAsync();

        // Act
        using var queryContext = _fixture.CreateContext();
        var queryRepo = new OrderRepository(queryContext);
        var result = await queryRepo.GetWithItemsAsync(order.Id);

        // Assert
        result.Should().NotBeNull();
        result!.OrderItems.Should().HaveCount(2);
        result.TotalAmount.Should().Be(45.00m);
    }

    [Fact]
    public async Task GetByUserIdAsync_HasOrders_ReturnsUserOrders()
    {
        // Arrange
        var order1 = new Order("user-123", new Address("St", "City", "ST", "12345", "US"));
        var order2 = new Order("user-123", new Address("St", "City", "ST", "12345", "US"));
        var order3 = new Order("other-user", new Address("St", "City", "ST", "12345", "US"));

        foreach (var o in new[] { order1, order2, order3 })
        {
            o.AddItem(Guid.NewGuid(), 1, 10.00m);
            await _repository.AddAsync(o);
        }
        await _repository.UnitOfWork.SaveChangesAsync();

        // Act
        using var queryContext = _fixture.CreateContext();
        var queryRepo = new OrderRepository(queryContext);
        var result = await queryRepo.GetByUserIdAsync("user-123");

        // Assert
        result.Should().HaveCount(2);
        result.Should().AllSatisfy(o => o.UserId.Should().Be("user-123"));
    }
}
```

---

## Functional Testing with TestServer

### Custom Web Application Factory

```csharp
/// <summary>
/// EN: Custom factory for functional tests.
/// VI: Factory tùy chỉnh cho functional tests.
/// </summary>
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly PostgreSqlContainer _container;
    
    public CustomWebApplicationFactory()
    {
        _container = new PostgreSqlBuilder()
            .WithImage("postgres:15-alpine")
            .Build();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        _container.StartAsync().GetAwaiter().GetResult();

        builder.ConfigureServices(services =>
        {
            // EN: Remove existing DbContext registration
            // VI: Xóa đăng ký DbContext hiện có
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);

            // EN: Add test database
            // VI: Thêm test database
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(_container.GetConnectionString()));

            // EN: Ensure database is created and migrated
            // VI: Đảm bảo database được tạo và migrate
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            db.Database.Migrate();
        });
    }

    protected override void Dispose(bool disposing)
    {
        _container.DisposeAsync().GetAwaiter().GetResult();
        base.Dispose(disposing);
    }
}
```

### API Functional Tests

```csharp
/// <summary>
/// EN: Functional tests for Orders API.
/// VI: Functional tests cho Orders API.
/// </summary>
public class OrdersApiTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory _factory;

    public OrdersApiTests(CustomWebApplicationFactory factory)
    {
        _factory = factory;
        _client = factory.CreateClient();

        // EN: Add test authentication
        // VI: Thêm authentication test
        _client.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Test", "user-123");
    }

    [Fact]
    public async Task CreateOrder_ValidRequest_Returns201WithOrderId()
    {
        // Arrange
        var request = new
        {
            ShippingAddress = new 
            { 
                Street = "123 Test St", 
                City = "Test City", 
                State = "TS", 
                PostalCode = "12345", 
                Country = "US" 
            },
            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);
        
        var content = await response.Content.ReadFromJsonAsync<ApiResponse<OrderResult>>();
        content.Should().NotBeNull();
        content!.Success.Should().BeTrue();
        content.Data!.OrderId.Should().NotBeEmpty();
    }

    [Fact]
    public async Task GetOrder_NotExists_Returns404()
    {
        // Act
        var response = await _client.GetAsync($"/api/v1/orders/{Guid.NewGuid()}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task CreateAndGetOrder_Workflow_ReturnsCreatedOrder()
    {
        // Arrange - Create order
        var createRequest = new
        {
            ShippingAddress = new { Street = "St", City = "City", State = "ST", PostalCode = "12345", Country = "US" },
            Items = new[] { new { ProductId = Guid.NewGuid(), Quantity = 1, UnitPrice = 50.00m } }
        };

        // Act - Create
        var createResponse = await _client.PostAsJsonAsync("/api/v1/orders", createRequest);
        var createResult = await createResponse.Content.ReadFromJsonAsync<ApiResponse<OrderResult>>();
        var orderId = createResult!.Data!.OrderId;

        // Act - Get
        var getResponse = await _client.GetAsync($"/api/v1/orders/{orderId}");
        var getResult = await getResponse.Content.ReadFromJsonAsync<ApiResponse<OrderDto>>();

        // Assert
        getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
        getResult!.Data!.Id.Should().Be(orderId);
        getResult.Data.TotalAmount.Should().Be(50.00m);
    }
}
```

---

## Test Data Builders

### Builder Pattern for Test Data

```csharp
/// <summary>
/// EN: Builder for creating test orders.
/// VI: Builder để tạo test orders.
/// </summary>
public class OrderBuilder
{
    private string _userId = "test-user";
    private Address _address = new("123 Test St", "Test City", "TS", "12345", "US");
    private readonly List<(Guid ProductId, int Quantity, decimal UnitPrice)> _items = new();
    private OrderStatus? _status;

    public OrderBuilder WithUserId(string userId)
    {
        _userId = userId;
        return this;
    }

    public OrderBuilder WithAddress(Address address)
    {
        _address = address;
        return this;
    }

    public OrderBuilder WithItem(Guid? productId = null, int quantity = 1, decimal unitPrice = 10.00m)
    {
        _items.Add((productId ?? Guid.NewGuid(), quantity, unitPrice));
        return this;
    }

    public OrderBuilder AsSubmitted()
    {
        _status = OrderStatus.Submitted;
        return this;
    }

    public Order Build()
    {
        var order = new Order(_userId, _address);

        foreach (var (productId, quantity, unitPrice) in _items)
        {
            order.AddItem(productId, quantity, unitPrice);
        }

        if (_status == OrderStatus.Submitted && _items.Any())
        {
            order.Submit();
        }

        return order;
    }
}

// EN: Usage / VI: Cách dùng
var order = new OrderBuilder()
    .WithUserId("user-123")
    .WithItem(quantity: 2, unitPrice: 15.00m)
    .WithItem(quantity: 1, unitPrice: 25.00m)
    .AsSubmitted()
    .Build();
```

---

## Resources / Tài Nguyên

- [xUnit Documentation](https://xunit.net/docs/getting-started/netcore/cmdline)
- [NSubstitute Documentation](https://nsubstitute.github.io/)
- [FluentAssertions](https://fluentassertions.com/)
- [Testcontainers for .NET](https://testcontainers.com/guides/getting-started-with-testcontainers-for-dotnet/)
- [Microsoft: Integration Testing](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests)
