Testing Mastery: Catch Bugs Early with Comprehensive Testing in ASP.NET Core
Master-ASP.NET-Core-Testing-Unit-Integration-UI-Tests-xUnit-Moq-Selenium-Comprehensive-Quality-Bug-Detection-CI-CD
ASP.NETCore,Testing,UnitTesting,IntegrationTesting,xUnit,Moq,Selenium,QualityAssurance,CI/CD,TestAutomation,QA,BugPrevention
📚 Table of Contents
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("test@example.com"); _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.
Powered By: FreeLearning365.com
.png)
0 Comments
thanks for your comments!