Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unit Test & Integration Test

Unit Test với xUnit

Cấu trúc Test

public class ProductServiceTests
{
    [Fact]
    public void GetProductById_ReturnsProduct_WhenProductExists()
    {
        // Arrange
        var productId = 1;
        var expectedProduct = new Product { Id = 1, Name = "Test" };
        
        var mockRepo = new Mock<IProductRepository>();
        mockRepo.Setup(r => r.GetByIdAsync(productId))
            .ReturnsAsync(expectedProduct);
            
        var service = new ProductService(mockRepo.Object);
        
        // Act
        var result = service.GetProductByIdAsync(productId).Result;
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedProduct.Name, result.Name);
    }
    
    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(100, true)]
    public void IsValidPrice_ReturnsExpected(int price, bool expected)
    {
        var service = new ProductService(null);
        var result = service.IsValidPrice(price);
        Assert.Equal(expected, result);
    }
}

Mocking với Moq

Basic Mocking

// Mock interface
var mockRepo = new Mock<IProductRepository>();

// Setup method
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
    .ReturnsAsync(new Product { Id = 1, Name = "Test" });

// Setup property
mockRepo.SetupGet(r => r.Count)
    .Returns(10);

// Verify calls
mockRepo.Verify(r => r.GetByIdAsync(It.IsAny<int>()), Times.Once);

Mocking Returns

// Sequential returns
var mockRepo = new Mock<IProductRepository>();
mockRepo.SetupSequence(r => r.GetNextAsync())
    .ReturnsAsync(new Product { Id = 1 })
    .ReturnsAsync(new Product { Id = 2 })
    .ReturnsAsync((Product)null);
    
// Callback
var products = new List<Product>();
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>()))
    .Callback<Product>(p => products.Add(p))
    .ReturnsAsync((Product p) => p);

Integration Testing

WebApplicationFactory

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove existing DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);
                
            // Add test DbContext
            services.AddDbContext<AppDbContext>(options =>
                options.UseInMemoryDatabase("TestDatabase"));
                
            // Build service provider
            var sp = services.BuildServiceProvider();
            
            // Seed data
            using var scope = sp.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            context.Products.Add(new Product { Name = "Test Product" });
            context.SaveChanges();
        });
    }
}

Test Class

public class ProductsControllerIntegrationTests 
    : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program> _factory;
    
    public ProductsControllerIntegrationTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsProducts()
    {
        // Act
        var response = await _client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<Product>>(content);
        
        Assert.NotNull(products);
        Assert.NotEmpty(products);
    }
}

Test Database

public class TestDatabaseFixture : IDisposable
{
    private readonly SqliteConnection _connection;
    public AppDbContext CreateContext() 
        => new(new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(_connection).Options);
    
    public TestDatabaseFixture()
    {
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();
        
        using var context = CreateContext();
        context.Database.EnsureCreated();
    }
    
    public void Dispose() => _connection.Dispose();
}

Test Patterns

Arrange-Act-Assert

[Fact]
public void CalculateTotal_ReturnsCorrectTotal()
{
    // Arrange
    var order = new Order
    {
        Items = new List<OrderItem>
        {
            new() { Price = 10.00m, Quantity = 2 },
            new() { Price = 5.00m, Quantity = 3 }
        }
    };
    
    // Act
    var total = order.CalculateTotal();
    
    // Assert
    Assert.Equal(35.00m, total);
}

AAA with Helper Methods

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _mockRepo;
    private readonly OrderService _service;
    
    public OrderServiceTests()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _service = new OrderService(_mockRepo.Object);
    }
    
    [Fact]
    public async Task CreateOrder_ReturnsOrder_WithGeneratedId()
    {
        // Arrange
        var order = CreateValidOrder();
        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
            .Callback<Order>(o => o.Id = 1)
            .ReturnsAsync((Order o) => o);
            
        // Act
        var result = await _service.CreateOrderAsync(order);
        
        // Assert
        Assert.NotEqual(Guid.Empty, result.Id);
        _mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
    }
    
    private Order CreateValidOrder() => new()
    {
        CustomerId = Guid.NewGuid(),
        Items = new List<OrderItem>
        {
            new() { ProductId = 1, Quantity = 1, Price = 10 }
        }
    };
}