
Previous article: ASP.NET Core Security Fortification: Master OWASP Best Practices & Threat Protection (Part - 28 of 40)
📚 Table of Contents
The Testing Pyramid Foundation
Unit Testing Mastery
Integration Testing Strategies
UI Testing with Selenium
Test-Driven Development
Mocking and Test Doubles
Testing ASP.NET Core APIs
Database Testing Approaches
Performance and Load Testing
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

Join the conversation! Your thoughts help the community grow.