ASP.NET Core  

Testing Mastery: Catch Bugs Early with Comprehensive Testing in ASP.NET Core (Part- 29 of 40)

testing

Previous article: ASP.NET Core Security Fortification: Master OWASP Best Practices & Threat Protection (Part - 28 of 40)

📚 Table of Contents

  1. The Testing Pyramid Foundation

  2. Unit Testing Mastery

  3. Integration Testing Strategies

  4. UI Testing with Selenium

  5. Test-Driven Development

  6. Mocking and Test Doubles

  7. Testing ASP.NET Core APIs

  8. Database Testing Approaches

  9. Performance and Load Testing

  10. CI/CD Testing Pipeline

1. The Testing Pyramid Foundation

1.1 Why Testing Matters in Real-World Applications

Real-Life Scenario: Imagine an e-commerce application where a bug in the shopping cart calculation causes customers to be overcharged by 10%. Without proper testing, this could go undetected for weeks, resulting in financial losses and damaged reputation.

  
    // Buggy implementation without tests
public class ShoppingCartService
{
    public decimal CalculateTotal(List<CartItem> items, string discountCode)
    {
        decimal total = items.Sum(item => item.Price * item.Quantity);
        
        // BUG: This should be subtraction, not addition
        if (!string.IsNullOrEmpty(discountCode))
        {
            total += 10.0m; // Should be total -= 10.0m
        }
        
        return total;
    }
}

// Test that would catch this bug
public class ShoppingCartServiceTests
{
    [Fact]
    public void CalculateTotal_WithValidDiscountCode_AppliesDiscountCorrectly()
    {
        // Arrange
        var service = new ShoppingCartService();
        var items = new List<CartItem>
        {
            new CartItem { Price = 100.0m, Quantity = 1 }
        };
        
        // Act
        var result = service.CalculateTotal(items, "SAVE10");
        
        // Assert
        Assert.Equal(90.0m, result); // This test would FAIL, catching the bug
    }
}
  

1.2 The Testing Pyramid Explained

The testing pyramid is a strategic approach to testing that emphasizes having many fast, inexpensive unit tests, fewer integration tests, and even fewer UI tests.

  
    // Testing Pyramid Structure
public class TestingPyramid
{
    // Layer 1: Unit Tests (70%)
    public void UnitTests()
    {
        // Fast, isolated, test individual components
        // Run in milliseconds, no external dependencies
    }
    
    // Layer 2: Integration Tests (20%)
    public void IntegrationTests()
    {
        // Test interactions between components
        // May involve databases, file systems, APIs
        // Run in seconds
    }
    
    // Layer 3: UI Tests (10%)
    public void UITests()
    {
        // Test complete user workflows
        // Slow, fragile, but essential for critical paths
        // Run in minutes
    }
}
  

1.3 Setting Up Your Testing Environment

  
    // Sample project structure
/*
MyApp/
├── src/
│   ├── MyApp.Web/                 # ASP.NET Core Web Application
│   ├── MyApp.Services/           # Business Logic Layer
│   └── MyApp.Data/               # Data Access Layer
└── tests/
    ├── MyApp.Web.Tests/          # Integration Tests
    ├── MyApp.Services.Tests/     # Unit Tests
    ├── MyApp.Data.Tests/         # Database Tests
    └── MyApp.UI.Tests/           # UI Tests
*/

// MyApp.Services.Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.6.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
    <PackageReference Include="Moq" Version="4.20.69" />
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
    <PackageReference Include="coverlet.collector" Version="6.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\MyApp.Services\MyApp.Services.csproj" />
  </ItemGroup>
</Project>
  

2. Unit Testing Mastery

2.1 xUnit Fundamentals

xUnit is a popular testing framework for .NET that provides a clean, extensible platform for writing tests.

  
    // Basic xUnit test structure
public class CalculatorTests
{
    [Fact]
    public void Add_TwoNumbers_ReturnsSum()
    {
        // Arrange
        var calculator = new Calculator();
        int a = 5, b = 3;
        
        // Act
        var result = calculator.Add(a, b);
        
        // Assert
        Assert.Equal(8, result);
    }
    
    [Theory]
    [InlineData(1, 1, 2)]
    [InlineData(2, 3, 5)]
    [InlineData(-1, 1, 0)]
    [InlineData(0, 0, 0)]
    public void Add_MultipleScenarios_ReturnsCorrectSum(int a, int b, int expected)
    {
        // Arrange
        var calculator = new Calculator();
        
        // Act
        var result = calculator.Add(a, b);
        
        // Assert
        Assert.Equal(expected, result);
    }
}

// Calculator implementation
public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
    public int Multiply(int a, int b) => a * b;
    public int Divide(int a, int b) => b == 0 ? throw new DivideByZeroException() : a / b;
}
  

2.2 Real-World Business Logic Testing

Scenario: Testing a loan approval system for a banking application

  
    public class LoanApplication
{
    public string ApplicantName { get; set; } = string.Empty;
    public decimal AnnualIncome { get; set; }
    public int CreditScore { get; set; }
    public decimal LoanAmount { get; set; }
    public int LoanTermMonths { get; set; }
    public EmploymentStatus EmploymentStatus { get; set; }
    public bool HasDefaultedBefore { get; set; }
}

public enum EmploymentStatus
{
    Unemployed,
    PartTime,
    FullTime,
    SelfEmployed
}

public class LoanApprovalService
{
    private const int MINIMUM_CREDIT_SCORE = 650;
    private const decimal MINIMUM_INCOME_RATIO = 0.25m;
    
    public LoanApprovalResult ProcessApplication(LoanApplication application)
    {
        if (application == null)
            throw new ArgumentNullException(nameof(application));
            
        var validationErrors = ValidateApplication(application);
        if (validationErrors.Any())
            return LoanApprovalResult.Rejected(validationErrors);
        
        var isApproved = IsEligibleForLoan(application);
        var interestRate = CalculateInterestRate(application);
        
        return isApproved 
            ? LoanApprovalResult.Approved(interestRate, application.LoanAmount)
            : LoanApprovalResult.Rejected(new[] { "Application does not meet criteria" });
    }
    
    private List<string> ValidateApplication(LoanApplication application)
    {
        var errors = new List<string>();
        
        if (string.IsNullOrWhiteSpace(application.ApplicantName))
            errors.Add("Applicant name is required");
            
        if (application.AnnualIncome <= 0)
            errors.Add("Annual income must be positive");
            
        if (application.LoanAmount <= 0)
            errors.Add("Loan amount must be positive");
            
        if (application.LoanTermMonths <= 0)
            errors.Add("Loan term must be positive");
            
        if (application.CreditScore < 300 || application.CreditScore > 850)
            errors.Add("Credit score must be between 300 and 850");
            
        return errors;
    }
    
    private bool IsEligibleForLoan(LoanApplication application)
    {
        // Basic eligibility criteria
        if (application.CreditScore < MINIMUM_CREDIT_SCORE)
            return false;
            
        if (application.HasDefaultedBefore)
            return false;
            
        if (application.EmploymentStatus == EmploymentStatus.Unemployed)
            return false;
            
        // Income verification
        decimal monthlyIncome = application.AnnualIncome / 12;
        decimal monthlyPayment = CalculateMonthlyPayment(
            application.LoanAmount, 
            CalculateInterestRate(application), 
            application.LoanTermMonths);
            
        return monthlyPayment <= monthlyIncome * MINIMUM_INCOME_RATIO;
    }
    
    private decimal CalculateInterestRate(LoanApplication application)
    {
        decimal baseRate = 0.05m; // 5% base rate
        
        // Adjust based on credit score
        if (application.CreditScore >= 800)
            baseRate -= 0.02m; // Excellent credit
        else if (application.CreditScore >= 700)
            baseRate -= 0.01m; // Good credit
        else if (application.CreditScore < 600)
            baseRate += 0.03m; // Poor credit
            
        // Adjust based on employment
        if (application.EmploymentStatus == EmploymentStatus.SelfEmployed)
            baseRate += 0.01m;
            
        return Math.Max(0.01m, baseRate); // Minimum 1% interest
    }
    
    private decimal CalculateMonthlyPayment(decimal principal, decimal annualRate, int termMonths)
    {
        decimal monthlyRate = annualRate / 12;
        decimal factor = (decimal)Math.Pow(1 + (double)monthlyRate, termMonths);
        return principal * monthlyRate * factor / (factor - 1);
    }
}

public class LoanApprovalResult
{
    public bool IsApproved { get; }
    public decimal? InterestRate { get; }
    public decimal? ApprovedAmount { get; }
    public IReadOnlyList<string> RejectionReasons { get; }
    
    private LoanApprovalResult(bool isApproved, decimal? interestRate, 
        decimal? approvedAmount, List<string> rejectionReasons)
    {
        IsApproved = isApproved;
        InterestRate = interestRate;
        ApprovedAmount = approvedAmount;
        RejectionReasons = rejectionReasons?.AsReadOnly() ?? new List<string>().AsReadOnly();
    }
    
    public static LoanApprovalResult Approved(decimal interestRate, decimal approvedAmount) =>
        new LoanApprovalResult(true, interestRate, approvedAmount, null);
        
    public static LoanApprovalResult Rejected(IEnumerable<string> reasons) =>
        new LoanApprovalResult(false, null, null, reasons?.ToList() ?? new List<string>());
}
  

2.3 Comprehensive Unit Tests for Loan Service

  
    public class LoanApprovalServiceTests
{
    private readonly LoanApprovalService _service;
    
    public LoanApprovalServiceTests()
    {
        _service = new LoanApprovalService();
    }
    
    [Fact]
    public void ProcessApplication_NullApplication_ThrowsArgumentNullException()
    {
        // Arrange
        LoanApplication application = null;
        
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => _service.ProcessApplication(application));
    }
    
    [Theory]
    [InlineData("", 50000, 700, 10000, 24, EmploymentStatus.FullTime, false, "Applicant name is required")]
    [InlineData("John Doe", 0, 700, 10000, 24, EmploymentStatus.FullTime, false, "Annual income must be positive")]
    [InlineData("John Doe", 50000, 700, 0, 24, EmploymentStatus.FullTime, false, "Loan amount must be positive")]
    [InlineData("John Doe", 50000, 700, 10000, 0, EmploymentStatus.FullTime, false, "Loan term must be positive")]
    [InlineData("John Doe", 50000, 200, 10000, 24, EmploymentStatus.FullTime, false, "Credit score must be between 300 and 850")]
    public void ProcessApplication_InvalidApplication_ReturnsRejectedWithErrors(
        string name, decimal income, int creditScore, decimal loanAmount, 
        int term, EmploymentStatus employment, bool hasDefaulted, string expectedError)
    {
        // Arrange
        var application = new LoanApplication
        {
            ApplicantName = name,
            AnnualIncome = income,
            CreditScore = creditScore,
            LoanAmount = loanAmount,
            LoanTermMonths = term,
            EmploymentStatus = employment,
            HasDefaultedBefore = hasDefaulted
        };
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.False(result.IsApproved);
        Assert.Contains(expectedError, result.RejectionReasons);
    }
    
    [Fact]
    public void ProcessApplication_ExcellentCredit_ReturnsApprovedWithLowInterest()
    {
        // Arrange
        var application = new LoanApplication
        {
            ApplicantName = "Jane Smith",
            AnnualIncome = 100000,
            CreditScore = 810, // Excellent credit
            LoanAmount = 20000,
            LoanTermMonths = 36,
            EmploymentStatus = EmploymentStatus.FullTime,
            HasDefaultedBefore = false
        };
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.True(result.IsApproved);
        Assert.NotNull(result.InterestRate);
        Assert.True(result.InterestRate <= 0.03m); // Should get low interest rate
        Assert.Equal(20000, result.ApprovedAmount);
    }
    
    [Fact]
    public void ProcessApplication_PoorCredit_ReturnsRejected()
    {
        // Arrange
        var application = new LoanApplication
        {
            ApplicantName = "Bob Wilson",
            AnnualIncome = 50000,
            CreditScore = 580, // Poor credit
            LoanAmount = 10000,
            LoanTermMonths = 24,
            EmploymentStatus = EmploymentStatus.FullTime,
            HasDefaultedBefore = false
        };
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.False(result.IsApproved);
        Assert.Contains("Application does not meet criteria", result.RejectionReasons);
    }
    
    [Fact]
    public void ProcessApplication_PreviousDefault_ReturnsRejected()
    {
        // Arrange
        var application = new LoanApplication
        {
            ApplicantName = "Sarah Johnson",
            AnnualIncome = 80000,
            CreditScore = 720, // Good credit
            LoanAmount = 15000,
            LoanTermMonths = 36,
            EmploymentStatus = EmploymentStatus.FullTime,
            HasDefaultedBefore = true // Previous default
        };
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.False(result.IsApproved);
    }
    
    [Fact]
    public void ProcessApplication_SelfEmployed_ReturnsApprovedWithHigherInterest()
    {
        // Arrange
        var application = new LoanApplication
        {
            ApplicantName = "Mike Entrepreneur",
            AnnualIncome = 120000,
            CreditScore = 750, // Good credit
            LoanAmount = 30000,
            LoanTermMonths = 48,
            EmploymentStatus = EmploymentStatus.SelfEmployed,
            HasDefaultedBefore = false
        };
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.True(result.IsApproved);
        Assert.True(result.InterestRate > 0.04m); // Should have higher rate for self-employed
    }
}
  

2.4 Advanced Unit Testing Patterns

  
    // Test data builders for complex objects
public class LoanApplicationBuilder
{
    private LoanApplication _application = new LoanApplication
    {
        ApplicantName = "Test Applicant",
        AnnualIncome = 75000,
        CreditScore = 700,
        LoanAmount = 25000,
        LoanTermMonths = 36,
        EmploymentStatus = EmploymentStatus.FullTime,
        HasDefaultedBefore = false
    };
    
    public LoanApplicationBuilder WithCreditScore(int score)
    {
        _application.CreditScore = score;
        return this;
    }
    
    public LoanApplicationBuilder WithIncome(decimal income)
    {
        _application.AnnualIncome = income;
        return this;
    }
    
    public LoanApplicationBuilder WithEmployment(EmploymentStatus status)
    {
        _application.EmploymentStatus = status;
        return this;
    }
    
    public LoanApplicationBuilder HasDefaulted(bool hasDefaulted = true)
    {
        _application.HasDefaultedBefore = hasDefaulted;
        return this;
    }
    
    public LoanApplication Build() => _application;
}

// Using the builder in tests
public class LoanApprovalServiceBuilderTests
{
    private readonly LoanApprovalService _service = new LoanApprovalService();
    
    [Fact]
    public void ProcessApplication_UsingBuilder_CreatesConsistentTestData()
    {
        // Arrange
        var application = new LoanApplicationBuilder()
            .WithCreditScore(780)
            .WithIncome(90000)
            .WithEmployment(EmploymentStatus.FullTime)
            .Build();
        
        // Act
        var result = _service.ProcessApplication(application);
        
        // Assert
        Assert.True(result.IsApproved);
    }
}

// Custom assertions for better test readability
public static class LoanApprovalResultAssertions
{
    public static void ShouldBeApproved(this LoanApprovalResult result, decimal? expectedAmount = null)
    {
        Assert.True(result.IsApproved, "Loan should be approved");
        if (expectedAmount.HasValue)
        {
            Assert.Equal(expectedAmount.Value, result.ApprovedAmount);
        }
    }
    
    public static void ShouldBeRejected(this LoanApprovalResult result, string expectedReason = null)
    {
        Assert.False(result.IsApproved, "Loan should be rejected");
        if (!string.IsNullOrEmpty(expectedReason))
        {
            Assert.Contains(expectedReason, result.RejectionReasons);
        }
    }
}

// Using custom assertions
[Fact]
public void ProcessApplication_WithCustomAssertions_ReadableTests()
{
    // Arrange
    var application = new LoanApplicationBuilder().Build();
    
    // Act
    var result = _service.ProcessApplication(application);
    
    // Assert
    result.ShouldBeApproved(25000);
}
  

3. Integration Testing Strategies

3.1 Testing with Real Databases

Scenario: Testing a product catalog service that interacts with a database

  
    public class ProductService
{
    private readonly ApplicationDbContext _context;
    
    public ProductService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<Product> CreateProductAsync(string name, string description, decimal price, int stockQuantity)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Product name is required", nameof(name));
            
        if (price <= 0)
            throw new ArgumentException("Price must be positive", nameof(price));
            
        var product = new Product
        {
            Name = name,
            Description = description,
            Price = price,
            StockQuantity = stockQuantity,
            CreatedAt = DateTime.UtcNow,
            IsActive = true
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        
        return product;
    }
    
    public async Task<List<Product>> SearchProductsAsync(string searchTerm, decimal? minPrice = null, 
        decimal? maxPrice = null, bool inStockOnly = false)
    {
        var query = _context.Products.AsQueryable();
        
        if (!string.IsNullOrWhiteSpace(searchTerm))
        {
            query = query.Where(p => p.Name.Contains(searchTerm) || 
                                    p.Description.Contains(searchTerm));
        }
        
        if (minPrice.HasValue)
        {
            query = query.Where(p => p.Price >= minPrice.Value);
        }
        
        if (maxPrice.HasValue)
        {
            query = query.Where(p => p.Price <= maxPrice.Value);
        }
        
        if (inStockOnly)
        {
            query = query.Where(p => p.StockQuantity > 0);
        }
        
        return await query.Where(p => p.IsActive)
                         .OrderBy(p => p.Name)
                         .ToListAsync();
    }
    
    public async Task UpdateStockAsync(int productId, int quantityChange)
    {
        var product = await _context.Products.FindAsync(productId);
        if (product == null)
            throw new ArgumentException($"Product with ID {productId} not found");
            
        product.StockQuantity += quantityChange;
        
        if (product.StockQuantity < 0)
            throw new InvalidOperationException("Insufficient stock");
            
        await _context.SaveChangesAsync();
    }
}

// Integration tests using TestContainers for database testing
public class ProductServiceIntegrationTests : IAsyncLifetime
{
    private readonly TestcontainerDatabase _database;
    private ApplicationDbContext _context;
    private ProductService _service;
    
    public ProductServiceIntegrationTests()
    {
        // Use TestContainers to spin up a real database for testing
        _database = new TestcontainersBuilder<PostgreSqlTestcontainer>()
            .WithDatabase(new PostgreSqlTestcontainerConfiguration
            {
                Database = "testdb",
                Username = "test",
                Password = "test"
            })
            .Build();
    }
    
    public async Task InitializeAsync()
    {
        await _database.StartAsync();
        
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(_database.ConnectionString)
            .Options;
            
        _context = new ApplicationDbContext(options);
        await _context.Database.EnsureCreatedAsync();
        
        _service = new ProductService(_context);
    }
    
    public async Task DisposeAsync()
    {
        await _context.DisposeAsync();
        await _database.DisposeAsync();
    }
    
    [Fact]
    public async Task CreateProductAsync_ValidProduct_CreatesAndReturnsProduct()
    {
        // Arrange
        var name = "Test Product";
        var description = "Test Description";
        var price = 29.99m;
        var stock = 100;
        
        // Act
        var result = await _service.CreateProductAsync(name, description, price, stock);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(name, result.Name);
        Assert.Equal(description, result.Description);
        Assert.Equal(price, result.Price);
        Assert.Equal(stock, result.StockQuantity);
        Assert.True(result.IsActive);
        
        // Verify in database
        var fromDb = await _context.Products.FindAsync(result.Id);
        Assert.NotNull(fromDb);
        Assert.Equal(name, fromDb.Name);
    }
    
    [Fact]
    public async Task SearchProductsAsync_WithFilters_ReturnsMatchingProducts()
    {
        // Arrange - Add test data
        var products = new[]
        {
            new Product { Name = "Laptop", Description = "Gaming laptop", Price = 999.99m, StockQuantity = 10 },
            new Product { Name = "Mouse", Description = "Wireless mouse", Price = 29.99m, StockQuantity = 0 },
            new Product { Name = "Keyboard", Description = "Mechanical keyboard", Price = 79.99m, StockQuantity = 5 }
        };
        
        _context.Products.AddRange(products);
        await _context.SaveChangesAsync();
        
        // Act
        var results = await _service.SearchProductsAsync("lap", minPrice: 500m, inStockOnly: true);
        
        // Assert
        Assert.Single(results);
        Assert.Equal("Laptop", results[0].Name);
    }
    
    [Fact]
    public async Task UpdateStockAsync_ValidChange_UpdatesStockQuantity()
    {
        // Arrange
        var product = new Product { Name = "Test", Price = 10m, StockQuantity = 50 };
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        
        // Act
        await _service.UpdateStockAsync(product.Id, -10);
        
        // Assert
        var updated = await _context.Products.FindAsync(product.Id);
        Assert.Equal(40, updated.StockQuantity);
    }
    
    [Fact]
    public async Task UpdateStockAsync_InsufficientStock_ThrowsException()
    {
        // Arrange
        var product = new Product { Name = "Test", Price = 10m, StockQuantity = 5 };
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => 
            _service.UpdateStockAsync(product.Id, -10));
    }
}
  

3.2 Testing Web APIs

  
    // API Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;
    
    public ProductsController(IProductService productService, ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }
    
    [HttpGet]
    public async Task<ActionResult<List<ProductDto>>> GetProducts([FromQuery] ProductSearchRequest request)
    {
        try
        {
            var products = await _productService.SearchProductsAsync(
                request.SearchTerm, 
                request.MinPrice, 
                request.MaxPrice, 
                request.InStockOnly);
                
            var dtos = products.Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Description = p.Description,
                Price = p.Price,
                StockQuantity = p.StockQuantity
            }).ToList();
            
            return Ok(dtos);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving products");
            return StatusCode(500, "An error occurred while retrieving products");
        }
    }
    
    [HttpPost]
    public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductRequest request)
    {
        try
        {
            var product = await _productService.CreateProductAsync(
                request.Name,
                request.Description,
                request.Price,
                request.StockQuantity);
                
            var dto = new ProductDto
            {
                Id = product.Id,
                Name = product.Name,
                Description = product.Description,
                Price = product.Price,
                StockQuantity = product.StockQuantity
            };
            
            return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, dto);
        }
        catch (ArgumentException ex)
        {
            return BadRequest(ex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating product");
            return StatusCode(500, "An error occurred while creating the product");
        }
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetProduct(int id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        if (product == null)
            return NotFound();
            
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            StockQuantity = product.StockQuantity
        };
    }
}

// Integration tests for API controller
public class ProductsControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;
    
    public ProductsControllerIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                // Replace real services with test doubles if needed
                services.AddScoped<IProductService, MockProductService>();
            });
        });
        
        _client = _factory.CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsSuccessWithProducts()
    {
        // Act
        var response = await _client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<ProductDto>>(content, 
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
            
        Assert.NotNull(products);
    }
    
    [Fact]
    public async Task CreateProduct_ValidRequest_ReturnsCreatedProduct()
    {
        // Arrange
        var request = new CreateProductRequest
        {
            Name = "Integration Test Product",
            Description = "Test Description",
            Price = 19.99m,
            StockQuantity = 50
        };
        
        var content = new StringContent(
            JsonSerializer.Serialize(request),
            Encoding.UTF8,
            "application/json");
        
        // Act
        var response = await _client.PostAsync("/api/products", content);
        
        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        
        var responseContent = await response.Content.ReadAsStringAsync();
        var product = JsonSerializer.Deserialize<ProductDto>(responseContent,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
            
        Assert.NotNull(product);
        Assert.Equal(request.Name, product.Name);
        Assert.Equal(request.Price, product.Price);
    }
    
    [Fact]
    public async Task GetProduct_NonExistentId_ReturnsNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/products/9999");
        
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
}
  

4. UI Testing with Selenium

4.1 Selenium WebDriver Setup

  
    public class WebUITests : IAsyncLifetime
{
    private IWebDriver _driver;
    private WebDriverWait _wait;
    
    public async Task InitializeAsync()
    {
        // Setup Chrome options
        var options = new ChromeOptions();
        options.AddArgument("--headless"); // Run in headless mode for CI
        options.AddArgument("--no-sandbox");
        options.AddArgument("--disable-dev-shm-usage");
        
        _driver = new ChromeDriver(options);
        _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
        
        await Task.CompletedTask;
    }
    
    public async Task DisposeAsync()
    {
        _driver?.Quit();
        _driver?.Dispose();
        await Task.CompletedTask;
    }
    
    [Fact]
    public void HomePage_LoadsSuccessfully_DisplaysWelcomeMessage()
    {
        // Arrange
        _driver.Navigate().GoToUrl("https://localhost:5001");
        
        // Act
        var welcomeElement = _wait.Until(d => 
            d.FindElement(By.CssSelector(".welcome-message")));
        
        // Assert
        Assert.Contains("Welcome", welcomeElement.Text);
    }
    
    [Fact]
    public void ProductSearch_FindsMatchingProducts_DisplaysResults()
    {
        // Arrange
        _driver.Navigate().GoToUrl("https://localhost:5001/products");
        
        // Act
        var searchBox = _driver.FindElement(By.Id("search-box"));
        searchBox.SendKeys("laptop");
        searchBox.SendKeys(Keys.Enter);
        
        // Wait for results
        var resultsContainer = _wait.Until(d => 
            d.FindElement(By.CssSelector(".search-results")));
            
        var productCards = resultsContainer.FindElements(By.CssSelector(".product-card"));
        
        // Assert
        Assert.True(productCards.Count > 0);
        
        // Verify at least one product contains "laptop" in name
        var hasLaptop = productCards.Any(card => 
            card.FindElement(By.CssSelector(".product-name"))
                .Text.ToLower().Contains("laptop"));
                
        Assert.True(hasLaptop);
    }
    
    [Fact]
    public void AddToCart_ValidProduct_UpdatesCartCounter()
    {
        // Arrange
        _driver.Navigate().GoToUrl("https://localhost:5001/products/1");
        
        // Act
        var addToCartButton = _wait.Until(d => 
            d.FindElement(By.CssSelector(".add-to-cart-btn")));
        addToCartButton.Click();
        
        // Wait for cart update
        var cartCounter = _wait.Until(d => 
            d.FindElement(By.CssSelector(".cart-counter")));
        
        // Assert
        Assert.Equal("1", cartCounter.Text);
    }
    
    [Fact]
    public void CheckoutProcess_CompleteFlow_ShowsConfirmation()
    {
        // Arrange - Add product to cart first
        _driver.Navigate().GoToUrl("https://localhost:5001/products/1");
        var addToCartButton = _wait.Until(d => 
            d.FindElement(By.CssSelector(".add-to-cart-btn")));
        addToCartButton.Click();
        
        // Act - Navigate to checkout
        var checkoutButton = _wait.Until(d => 
            d.FindElement(By.CssSelector(".checkout-btn")));
        checkoutButton.Click();
        
        // Fill checkout form
        _wait.Until(d => d.FindElement(By.Id("email"))).SendKeys("[email protected]");
        _driver.FindElement(By.Id("firstName")).SendKeys("John");
        _driver.FindElement(By.Id("lastName")).SendKeys("Doe");
        _driver.FindElement(By.Id("address")).SendKeys("123 Main St");
        _driver.FindElement(By.Id("city")).SendKeys("Test City");
        _driver.FindElement(By.Id("zipCode")).SendKeys("12345");
        
        // Submit order
        var submitButton = _driver.FindElement(By.CssSelector(".submit-order-btn"));
        submitButton.Click();
        
        // Assert
        var confirmation = _wait.Until(d => 
            d.FindElement(By.CssSelector(".order-confirmation")));
            
        Assert.Contains("Thank you for your order", confirmation.Text);
    }
}
  

4.2 Page Object Pattern for Maintainable UI Tests

  
    // Base page class
public abstract class BasePage
{
    protected IWebDriver Driver;
    protected WebDriverWait Wait;
    
    protected BasePage(IWebDriver driver)
    {
        Driver = driver;
        Wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }
    
    protected IWebElement FindElement(By locator) => Wait.Until(d => d.FindElement(locator));
    protected IReadOnlyCollection<IWebElement> FindElements(By locator) => Driver.FindElements(locator);
    protected void Click(By locator) => FindElement(locator).Click();
    protected void Type(By locator, string text) => FindElement(locator).SendKeys(text);
}

// Home page
public class HomePage : BasePage
{
    private By WelcomeMessage => By.CssSelector(".welcome-message");
    private By ProductSearchBox => By.Id("search-box");
    private By SearchButton => By.CssSelector(".search-btn");
    private By CartCounter => By.CssSelector(".cart-counter");
    private By NavigationMenu => By.CssSelector(".nav-menu");
    
    public HomePage(IWebDriver driver) : base(driver) { }
    
    public void NavigateTo() => Driver.Navigate().GoToUrl("https://localhost:5001");
    public string GetWelcomeMessage() => FindElement(WelcomeMessage).Text;
    public string GetCartCount() => FindElement(CartCounter).Text;
    
    public SearchResultsPage SearchForProduct(string searchTerm)
    {
        Type(ProductSearchBox, searchTerm);
        Click(SearchButton);
        return new SearchResultsPage(Driver);
    }
    
    public ProductPage NavigateToProduct(int productId)
    {
        Driver.Navigate().GoToUrl($"https://localhost:5001/products/{productId}");
        return new ProductPage(Driver);
    }
}

// Search results page
public class SearchResultsPage : BasePage
{
    private By ResultsContainer => By.CssSelector(".search-results");
    private By ProductCards => By.CssSelector(".product-card");
    private By ProductNames => By.CssSelector(".product-name");
    private By NoResultsMessage => By.CssSelector(".no-results");
    
    public SearchResultsPage(IWebDriver driver) : base(driver) { }
    
    public int GetResultCount() => FindElements(ProductCards).Count;
    public bool HasResults() => GetResultCount() > 0;
    public string GetNoResultsMessage() => FindElement(NoResultsMessage).Text;
    
    public List<string> GetProductNames() => 
        FindElements(ProductNames).Select(e => e.Text).ToList();
        
    public ProductPage ClickProduct(int index)
    {
        var products = FindElements(ProductCards);
        if (index < products.Count)
        {
            products.ElementAt(index).Click();
            return new ProductPage(Driver);
        }
        throw new IndexOutOfRangeException($"Product index {index} out of range");
    }
}

// Product page
public class ProductPage : BasePage
{
    private By ProductName => By.CssSelector(".product-name");
    private By ProductPrice => By.CssSelector(".product-price");
    private By AddToCartButton => By.CssSelector(".add-to-cart-btn");
    private By CartCounter => By.CssSelector(".cart-counter");
    private By StockStatus => By.CssSelector(".stock-status");
    
    public ProductPage(IWebDriver driver) : base(driver) { }
    
    public string GetProductName() => FindElement(ProductName).Text;
    public string GetProductPrice() => FindElement(ProductPrice).Text;
    public string GetStockStatus() => FindElement(StockStatus).Text;
    
    public void AddToCart()
    {
        Click(AddToCartButton);
        // Wait for cart to update
        Wait.Until(d => FindElement(CartCounter).Text != "0");
    }
}

// Refactored tests using page objects
public class WebUIPageObjectTests : IAsyncLifetime
{
    private IWebDriver _driver;
    private HomePage _homePage;
    
    public async Task InitializeAsync()
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless");
        _driver = new ChromeDriver(options);
        _homePage = new HomePage(_driver);
        
        await Task.CompletedTask;
    }
    
    public async Task DisposeAsync()
    {
        _driver?.Quit();
        _driver?.Dispose();
        await Task.CompletedTask;
    }
    
    [Fact]
    public void ProductSearch_UsingPageObjects_FindsMatchingProducts()
    {
        // Arrange
        _homePage.NavigateTo();
        
        // Act
        var resultsPage = _homePage.SearchForProduct("laptop");
        
        // Assert
        Assert.True(resultsPage.HasResults());
        var productNames = resultsPage.GetProductNames();
        Assert.Contains(productNames, name => name.ToLower().Contains("laptop"));
    }
    
    [Fact]
    public void AddToCart_UsingPageObjects_UpdatesCartCounter()
    {
        // Arrange
        _homePage.NavigateTo();
        var productPage = _homePage.NavigateToProduct(1);
        
        // Act
        productPage.AddToCart();
        
        // Assert
        Assert.Equal("1", _homePage.GetCartCount());
    }
    
    [Fact]
    public void CompletePurchaseFlow_UsingPageObjects_CompletesSuccessfully()
    {
        // This would continue with checkout page objects...
        // Demonstrating the maintainability of page object pattern
    }
}
  

5. Test-Driven Development

5.1 TDD Workflow: Red-Green-Refactor

Real-World Scenario: Developing a payment processing service using TDD

  
    // Step 1: Write a failing test (RED)
public class PaymentServiceTests
{
    [Fact]
    public void ProcessPayment_ValidCreditCard_ReturnsSuccess()
    {
        // Arrange
        var service = new PaymentService();
        var request = new PaymentRequest
        {
            Amount = 100.00m,
            CreditCardNumber = "4111111111111111",
            ExpiryMonth = 12,
            ExpiryYear = 2025,
            CVV = "123"
        };
        
        // Act
        var result = service.ProcessPayment(request);
        
        // Assert
        Assert.True(result.IsSuccessful);
        Assert.NotNull(result.TransactionId);
    }
}

// Step 2: Implement minimum code to pass test (GREEN)
public class PaymentService
{
    public PaymentResult ProcessPayment(PaymentRequest request)
    {
        // Minimal implementation to pass the test
        return new PaymentResult
        {
            IsSuccessful = true,
            TransactionId = Guid.NewGuid().ToString()
        };
    }
}

// Step 3: Refactor and add more tests
[Fact]
public void ProcessPayment_InvalidCreditCard_ReturnsFailure()
{
    // Arrange
    var service = new PaymentService();
    var request = new PaymentRequest
    {
        Amount = 100.00m,
        CreditCardNumber = "1234", // Invalid
        ExpiryMonth = 12,
        ExpiryYear = 2025,
        CVV = "123"
    };
    
    // Act
    var result = service.ProcessPayment(request);
    
    // Assert
    Assert.False(result.IsSuccessful);
    Assert.Contains("Invalid credit card", result.ErrorMessage);
}
  

5.2 Comprehensive TDD Example: Shopping Cart

  
    // Shopping cart TDD development
public class ShoppingCartTests
{
    [Fact]
    public void NewCart_IsEmpty()
    {
        // Arrange & Act
        var cart = new ShoppingCart();
        
        // Assert
        Assert.Empty(cart.Items);
        Assert.Equal(0, cart.TotalItems);
        Assert.Equal(0m, cart.TotalPrice);
    }
    
    [Fact]
    public void AddItem_ValidItem_AddsToCart()
    {
        // Arrange
        var cart = new ShoppingCart();
        var product = new Product { Id = 1, Name = "Test", Price = 10.0m };
        
        // Act
        cart.AddItem(product, 2);
        
        // Assert
        Assert.Single(cart.Items);
        Assert.Equal(2, cart.TotalItems);
        Assert.Equal(20.0m, cart.TotalPrice);
    }
    
    [Fact]
    public void AddItem_ExistingProduct_UpdatesQuantity()
    {
        // Arrange
        var cart = new ShoppingCart();
        var product = new Product { Id = 1, Name = "Test", Price = 10.0m };
        cart.AddItem(product, 1);
        
        // Act
        cart.AddItem(product, 2);
        
        // Assert
        Assert.Single(cart.Items);
        Assert.Equal(3, cart.TotalItems);
        Assert.Equal(30.0m, cart.TotalPrice);
    }
    
    [Fact]
    public void RemoveItem_ExistingItem_RemovesFromCart()
    {
        // Arrange
        var cart = new ShoppingCart();
        var product = new Product { Id = 1, Name = "Test", Price = 10.0m };
        cart.AddItem(product, 2);
        
        // Act
        cart.RemoveItem(product.Id);
        
        // Assert
        Assert.Empty(cart.Items);
        Assert.Equal(0, cart.TotalItems);
        Assert.Equal(0m, cart.TotalPrice);
    }
    
    [Fact]
    public void Clear_ItemsInCart_EmptiesCart()
    {
        // Arrange
        var cart = new ShoppingCart();
        cart.AddItem(new Product { Id = 1, Name = "Test1", Price = 10.0m }, 1);
        cart.AddItem(new Product { Id = 2, Name = "Test2", Price = 20.0m }, 2);
        
        // Act
        cart.Clear();
        
        // Assert
        Assert.Empty(cart.Items);
        Assert.Equal(0, cart.TotalItems);
        Assert.Equal(0m, cart.TotalPrice);
    }
    
    [Fact]
    public void ApplyDiscount_ValidPercentage_ReducesTotal()
    {
        // Arrange
        var cart = new ShoppingCart();
        cart.AddItem(new Product { Id = 1, Name = "Test", Price = 100.0m }, 1);
        
        // Act
        cart.ApplyDiscount(10); // 10% discount
        
        // Assert
        Assert.Equal(90.0m, cart.TotalPrice);
    }
    
    [Theory]
    [InlineData(-10)]
    [InlineData(110)]
    public void ApplyDiscount_InvalidPercentage_ThrowsException(int discount)
    {
        // Arrange
        var cart = new ShoppingCart();
        
        // Act & Assert
        Assert.Throws<ArgumentException>(() => cart.ApplyDiscount(discount));
    }
}

// Implementation after TDD
public class ShoppingCart
{
    private readonly List<CartItem> _items = new();
    private decimal _discountPercentage = 0;
    
    public IReadOnlyList<CartItem> Items => _items.AsReadOnly();
    
    public int TotalItems => _items.Sum(item => item.Quantity);
    
    public decimal TotalPrice
    {
        get
        {
            var subtotal = _items.Sum(item => item.TotalPrice);
            return subtotal * (1 - _discountPercentage / 100);
        }
    }
    
    public void AddItem(Product product, int quantity)
    {
        if (product == null)
            throw new ArgumentNullException(nameof(product));
            
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive", nameof(quantity));
        
        var existingItem = _items.FirstOrDefault(item => item.ProductId == product.Id);
        if (existingItem != null)
        {
            existingItem.Quantity += quantity;
        }
        else
        {
            _items.Add(new CartItem
            {
                ProductId = product.Id,
                ProductName = product.Name,
                UnitPrice = product.Price,
                Quantity = quantity
            });
        }
    }
    
    public void RemoveItem(int productId)
    {
        var item = _items.FirstOrDefault(i => i.ProductId == productId);
        if (item != null)
        {
            _items.Remove(item);
        }
    }
    
    public void Clear()
    {
        _items.Clear();
        _discountPercentage = 0;
    }
    
    public void ApplyDiscount(int percentage)
    {
        if (percentage < 0 || percentage > 100)
            throw new ArgumentException("Discount percentage must be between 0 and 100");
            
        _discountPercentage = percentage;
    }
}

public class CartItem
{
    public int ProductId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public decimal UnitPrice { get; set; }
    public int Quantity { get; set; }
    public decimal TotalPrice => UnitPrice * Quantity;
}
  

Note: This is a comprehensive excerpt from the full blog post. The complete article would continue with:

6. Mocking and Test Doubles

  • Moq Framework Deep Dive

  • Mocking External Dependencies

  • Verification and Behavior Testing

  • Custom Test Doubles

7. Testing ASP.NET Core APIs

  • Controller Testing Strategies

  • Authentication and Authorization Testing

  • API Integration Testing

  • Response Validation

8. Database Testing Approaches

  • Entity Framework Core Testing

  • Repository Pattern Testing

  • In-Memory Database Testing

  • Migration Testing

9. Performance and Load Testing

  • Benchmark Testing with Benchmark.NET

  • Load Testing Strategies

  • Performance Monitoring

  • Stress Testing

10. CI/CD Testing Pipeline

  • Automated Testing in GitHub Actions

  • Azure DevOps Testing Pipelines

  • Quality Gates and Reporting

  • Test Environment Management

Each section includes real-world examples, code samples, best practices, and common pitfalls to avoid.