You are viewing a preview of this lesson. Sign in to start learning
Back to ASP.NET with .NET 10

Testing & Quality Assurance

Comprehensive testing strategies for Minimal APIs

Testing & Quality Assurance in ASP.NET with .NET 10

Master testing and quality assurance in ASP.NET applications with free flashcards and spaced repetition practice. This lesson covers unit testing, integration testing, test-driven development (TDD), mocking frameworks, and automated testing strategiesโ€”essential skills for building reliable, maintainable web applications in .NET 10.

Welcome to Testing & Quality Assurance ๐Ÿงช

Building robust ASP.NET applications requires more than just writing code that worksโ€”it requires confidence that your code will continue to work as requirements evolve. Testing and quality assurance provide that confidence through systematic verification of your application's behavior.

In modern .NET development, testing isn't an afterthoughtโ€”it's an integral part of the development process. Whether you're building RESTful APIs, Blazor applications, or enterprise systems, a comprehensive testing strategy ensures your code is reliable, maintainable, and production-ready.

๐Ÿ’ก Did you know? Microsoft's own .NET team maintains over 250,000 automated tests for the framework itself! This massive test suite runs continuously, catching regressions before they reach developers.

Core Concepts in Testing & Quality Assurance ๐Ÿ’ป

Understanding the Testing Pyramid ๐Ÿ”บ

The testing pyramid represents the ideal distribution of different test types:

          โ•ฑโ•ฒ
         โ•ฑ  โ•ฒ
        โ•ฑ E2E โ•ฒ         โ† Few, slow, expensive
       โ•ฑโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฒ
      โ•ฑ          โ•ฒ
     โ•ฑIntegration โ•ฒ     โ† Moderate number
    โ•ฑโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฒ
   โ•ฑ                โ•ฒ
  โ•ฑ   Unit Tests     โ•ฒ  โ† Many, fast, cheap
 โ•ฑโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฒ

Unit Tests form the foundationโ€”they're fast, isolated, and test individual components. Integration Tests verify that components work together correctly. End-to-End (E2E) Tests validate entire user workflows but are slower and more brittle.

Unit Testing in .NET 10 ๐ŸŽฏ

Unit testing focuses on testing individual methods or classes in isolation. .NET 10 supports multiple testing frameworks:

  • xUnit (most popular in .NET community)
  • NUnit (feature-rich, Java heritage)
  • MSTest (Microsoft's framework, tight Visual Studio integration)
FrameworkStrengthsBest For
xUnitModern, extensible, parallel executionNew projects, microservices
NUnitRich assertions, extensive attributesComplex test scenarios
MSTestEnterprise support, IDE integrationCorporate environments

Test-Driven Development (TDD) ๐Ÿ”„

TDD is a development methodology where you write tests before implementation:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         TDD CYCLE                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

    โ”Œโ”€โ”€โ†’ ๐Ÿ”ด Write Failing Test
    โ”‚           โ”‚
    โ”‚           โ†“
    โ”‚    ๐ŸŸข Write Minimal Code
    โ”‚       (make test pass)
    โ”‚           โ”‚
    โ”‚           โ†“
    โ””โ”€โ”€โ”€โ”€  ๐Ÿ”ต Refactor
         (improve code)

๐Ÿง  Memory Device - RED, GREEN, REFACTOR: Think of a traffic lightโ€”Red (stop and think about requirements), Green (go ahead and implement), Blue (refactor = cool down and improve).

Mocking and Test Doubles ๐ŸŽญ

When testing units in isolation, you need to replace dependencies with test doubles:

TypePurposeExample
MockVerify interactions occurredDid SaveAsync() get called?
StubReturn predefined dataReturn fake user data
FakeWorking simplified implementationIn-memory database
SpyRecord information about callsHow many times was method called?

The most popular mocking framework for .NET is Moq, though alternatives like NSubstitute and FakeItEasy are also excellent choices.

Arrange-Act-Assert (AAA) Pattern ๐Ÿ“‹

The AAA pattern provides structure for well-organized tests:

// ARRANGE - Set up test data and dependencies
var calculator = new Calculator();
var x = 5;
var y = 3;

// ACT - Execute the method under test
var result = calculator.Add(x, y);

// ASSERT - Verify the outcome
Assert.Equal(8, result);

๐Ÿ’ก Tip: Some developers add a fourth "A" for Annihilate (cleanup), though modern test frameworks usually handle this automatically with IDisposable.

Integration Testing in ASP.NET Core ๐Ÿ”—

Integration tests verify that multiple components work together correctly. ASP.NET Core provides WebApplicationFactory<TStartup> for testing entire HTTP pipelines:

public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    
    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }
}

This creates a test server that hosts your application in-memoryโ€”no actual HTTP requests leave your machine!

Code Coverage Metrics ๐Ÿ“Š

Code coverage measures what percentage of your code is exercised by tests:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚     Code Coverage Types              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                      โ”‚
โ”‚  ๐Ÿ“„ Line Coverage                   โ”‚
โ”‚  What % of lines executed?           โ”‚
โ”‚                                      โ”‚
โ”‚  ๐Ÿ”€ Branch Coverage                  โ”‚
โ”‚  What % of if/else paths tested?     โ”‚
โ”‚                                      โ”‚
โ”‚  ๐ŸŽฏ Method Coverage                  โ”‚
โ”‚  What % of methods called?           โ”‚
โ”‚                                      โ”‚
โ”‚  ๐Ÿ”„ Path Coverage                    โ”‚
โ”‚  What % of execution paths tested?   โ”‚
โ”‚                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โš ๏ธ Warning: High coverage doesn't guarantee quality tests! 100% coverage with poor assertions is worthless. Focus on meaningful coverage of critical paths.

Continuous Integration & Testing ๐Ÿš€

CI/CD pipelines automatically run tests on every commit:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚    CI/CD PIPELINE FOR ASP.NET             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  ๐Ÿ’ป Code Commit
       โ”‚
       โ†“
  ๐Ÿ”จ Build Project
       โ”‚
       โ†“
  ๐Ÿงช Run Unit Tests โ”€โ”€โ”€โ”€โ†’ โŒ Fail โ†’ ๐Ÿšซ Stop
       โ”‚                              โ†“
       โ†“ โœ… Pass                  Fix Code
  ๐Ÿ”— Run Integration Tests
       โ”‚
       โ†“ โœ… Pass
  ๐Ÿ“ฆ Package Application
       โ”‚
       โ†“
  ๐Ÿš€ Deploy to Staging
       โ”‚
       โ†“
  ๐ŸŽญ Run E2E Tests
       โ”‚
       โ†“ โœ… Pass
  โœจ Deploy to Production

Popular CI/CD platforms for .NET:

  • GitHub Actions (excellent .NET support)
  • Azure DevOps (tight .NET integration)
  • GitLab CI (open-source friendly)

Testing Controllers & APIs ๐ŸŽฎ

ASP.NET controllers have specific testing considerations:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;
    
    public ProductsController(IProductService service)
    {
        _service = service;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var product = await _service.GetProductAsync(id);
        if (product == null)
            return NotFound();
        return Ok(product);
    }
}

You'll test:

  • Return types (ActionResult)
  • Status codes (200, 404, 400, etc.)
  • Response data (serialized objects)
  • Dependency interactions (was service called correctly?)

Testing Async/Await Code โฑ๏ธ

Modern ASP.NET is heavily asynchronous. Test frameworks fully support async tests:

[Fact]
public async Task GetProductAsync_ValidId_ReturnsProduct()
{
    // Arrange
    var mockService = new Mock<IProductService>();
    mockService.Setup(s => s.GetProductAsync(1))
               .ReturnsAsync(new Product { Id = 1, Name = "Widget" });
    
    // Act
    var result = await mockService.Object.GetProductAsync(1);
    
    // Assert
    Assert.NotNull(result);
    Assert.Equal("Widget", result.Name);
}

๐Ÿ’ก Tip: Always use async/await in testsโ€”don't call .Result or .Wait(), as they can cause deadlocks in test runners!

Parameterized Tests ๐Ÿ“

Test the same logic with different inputs using theory tests:

[Theory]
[InlineData(0, 0, 0)]
[InlineData(1, 1, 2)]
[InlineData(-1, -1, -2)]
[InlineData(100, 200, 300)]
public void Add_VariousInputs_ReturnsSum(int x, int y, int expected)
{
    var calculator = new Calculator();
    var result = calculator.Add(x, y);
    Assert.Equal(expected, result);
}

This runs four separate tests with one method definitionโ€”much more maintainable!

Test Fixtures & Setup/Teardown ๐Ÿ”ง

Fixtures share setup code across multiple tests:

public class DatabaseFixture : IDisposable
{
    public ApplicationDbContext Context { get; private set; }
    
    public DatabaseFixture()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase("TestDb")
            .Options;
        Context = new ApplicationDbContext(options);
    }
    
    public void Dispose()
    {
        Context.Dispose();
    }
}

public class UserRepositoryTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _fixture;
    
    public UserRepositoryTests(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }
}

๐Ÿ”„ Fixture lifecycle:

  • IClassFixture<T>: One instance shared across all tests in the class
  • ICollectionFixture<T>: Shared across multiple test classes
  • Constructor/Dispose: Run before/after each test

Examples: Real-World Testing Scenarios ๐ŸŒ

Example 1: Unit Testing a Service with Moq

Let's test a UserService that depends on a repository:

public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<bool> EmailExistsAsync(string email);
    Task SaveAsync(User user);
}

public class UserService
{
    private readonly IUserRepository _repository;
    
    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }
    
    public async Task<Result> RegisterUserAsync(string email, string name)
    {
        if (await _repository.EmailExistsAsync(email))
            return Result.Failure("Email already registered");
        
        var user = new User { Email = email, Name = name };
        await _repository.SaveAsync(user);
        return Result.Success();
    }
}

Test implementation:

public class UserServiceTests
{
    [Fact]
    public async Task RegisterUserAsync_NewEmail_SavesUser()
    {
        // Arrange
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(r => r.EmailExistsAsync(It.IsAny<string>()))
                .ReturnsAsync(false);
        mockRepo.Setup(r => r.SaveAsync(It.IsAny<User>()))
                .Returns(Task.CompletedTask);
        
        var service = new UserService(mockRepo.Object);
        
        // Act
        var result = await service.RegisterUserAsync("test@example.com", "John Doe");
        
        // Assert
        Assert.True(result.IsSuccess);
        mockRepo.Verify(r => r.SaveAsync(It.Is<User>(u => 
            u.Email == "test@example.com" && 
            u.Name == "John Doe")), Times.Once);
    }
    
    [Fact]
    public async Task RegisterUserAsync_DuplicateEmail_ReturnsFailure()
    {
        // Arrange
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(r => r.EmailExistsAsync("duplicate@example.com"))
                .ReturnsAsync(true);
        
        var service = new UserService(mockRepo.Object);
        
        // Act
        var result = await service.RegisterUserAsync("duplicate@example.com", "Jane Doe");
        
        // Assert
        Assert.False(result.IsSuccess);
        Assert.Equal("Email already registered", result.ErrorMessage);
        mockRepo.Verify(r => r.SaveAsync(It.IsAny<User>()), Times.Never);
    }
}

๐ŸŽฏ Key Points:

  • We mock the repository to isolate the service
  • We verify that methods are called with expected parameters
  • We test both success and failure paths
  • We use Times.Once and Times.Never to ensure correct behavior

Example 2: Integration Testing an API Endpoint

Test a complete HTTP request/response cycle:

public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;
    
    public ProductsApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace real database with in-memory version
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                if (descriptor != null)
                    services.Remove(descriptor);
                
                services.AddDbContext<AppDbContext>(options =>
                {
                    options.UseInMemoryDatabase("TestDb");
                });
            });
        });
        
        _client = _factory.CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsSuccessAndProducts()
    {
        // Arrange - Seed test data
        using (var scope = _factory.Services.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            context.Products.AddRange(
                new Product { Id = 1, Name = "Widget", Price = 9.99m },
                new Product { Id = 2, Name = "Gadget", Price = 19.99m }
            );
            await context.SaveChangesAsync();
        }
        
        // Act
        var response = await _client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<Product>>(content);
        
        Assert.NotNull(products);
        Assert.Equal(2, products.Count);
    }
    
    [Fact]
    public async Task CreateProduct_ValidData_ReturnsCreated()
    {
        // Arrange
        var newProduct = new { Name = "New Widget", Price = 14.99 };
        var content = new StringContent(
            JsonSerializer.Serialize(newProduct),
            Encoding.UTF8,
            "application/json");
        
        // Act
        var response = await _client.PostAsync("/api/products", content);
        
        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.NotNull(response.Headers.Location);
    }
}

๐ŸŒ Real-World Analogy: Integration testing is like testing a car on a closed trackโ€”you're using real components (engine, transmission, brakes) but in a controlled environment (test database, test server) before taking it on public roads (production).

Example 3: Testing with Realistic Test Data

Use the Builder Pattern for maintainable test data:

public class ProductBuilder
{
    private int _id = 1;
    private string _name = "Default Product";
    private decimal _price = 10.00m;
    private string _category = "General";
    private bool _inStock = true;
    
    public ProductBuilder WithId(int id)
    {
        _id = id;
        return this;
    }
    
    public ProductBuilder WithName(string name)
    {
        _name = name;
        return this;
    }
    
    public ProductBuilder WithPrice(decimal price)
    {
        _price = price;
        return this;
    }
    
    public ProductBuilder OutOfStock()
    {
        _inStock = false;
        return this;
    }
    
    public Product Build()
    {
        return new Product
        {
            Id = _id,
            Name = _name,
            Price = _price,
            Category = _category,
            InStock = _inStock
        };
    }
}

// Usage in tests
[Fact]
public void CalculateTotal_OutOfStockProducts_ExcludesFromTotal()
{
    // Arrange
    var products = new List<Product>
    {
        new ProductBuilder().WithPrice(10.00m).Build(),
        new ProductBuilder().WithPrice(20.00m).OutOfStock().Build(),
        new ProductBuilder().WithPrice(15.00m).Build()
    };
    
    var calculator = new PriceCalculator();
    
    // Act
    var total = calculator.CalculateTotal(products);
    
    // Assert
    Assert.Equal(25.00m, total); // Only in-stock items
}

๐Ÿ’ก Benefit: The Builder pattern makes tests readable and maintainableโ€”you only specify what's different from the default, not every single property!

Example 4: Testing Exception Handling

Verify your code handles errors gracefully:

public class FileService
{
    public async Task<string> ReadFileAsync(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
            throw new ArgumentException("Path cannot be empty", nameof(path));
        
        if (!File.Exists(path))
            throw new FileNotFoundException($"File not found: {path}");
        
        return await File.ReadAllTextAsync(path);
    }
}

public class FileServiceTests
{
    [Fact]
    public async Task ReadFileAsync_EmptyPath_ThrowsArgumentException()
    {
        // Arrange
        var service = new FileService();
        
        // Act & Assert
        var exception = await Assert.ThrowsAsync<ArgumentException>(
            () => service.ReadFileAsync(""));
        
        Assert.Equal("path", exception.ParamName);
    }
    
    [Fact]
    public async Task ReadFileAsync_NonExistentFile_ThrowsFileNotFoundException()
    {
        // Arrange
        var service = new FileService();
        var fakePath = "/nonexistent/file.txt";
        
        // Act & Assert
        var exception = await Assert.ThrowsAsync<FileNotFoundException>(
            () => service.ReadFileAsync(fakePath));
        
        Assert.Contains(fakePath, exception.Message);
    }
    
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task ReadFileAsync_InvalidPaths_ThrowsArgumentException(string invalidPath)
    {
        var service = new FileService();
        await Assert.ThrowsAsync<ArgumentException>(
            () => service.ReadFileAsync(invalidPath));
    }
}

โš ๏ธ Testing Philosophy: If your code can throw an exception, your tests must verify that behavior. Untested error paths are time bombs waiting to explode in production!

Common Mistakes to Avoid โš ๏ธ

1. Testing Implementation Instead of Behavior โŒ

Wrong Approach:

[Fact]
public void ProcessOrder_CallsRepositoryThreeTimes()
{
    var mockRepo = new Mock<IOrderRepository>();
    var service = new OrderService(mockRepo.Object);
    
    service.ProcessOrder(orderId: 123);
    
    // Testing HOW it works, not WHAT it does
    mockRepo.Verify(r => r.GetById(123), Times.Once);
    mockRepo.Verify(r => r.Update(It.IsAny<Order>()), Times.Once);
    mockRepo.Verify(r => r.Save(), Times.Once);
}

Better Approach:

[Fact]
public void ProcessOrder_MarksOrderAsProcessed()
{
    var mockRepo = new Mock<IOrderRepository>();
    var order = new Order { Id = 123, Status = OrderStatus.Pending };
    mockRepo.Setup(r => r.GetById(123)).Returns(order);
    
    var service = new OrderService(mockRepo.Object);
    service.ProcessOrder(123);
    
    // Testing WHAT changed, not HOW
    Assert.Equal(OrderStatus.Processed, order.Status);
}

๐ŸŽฏ Principle: Test the outcome, not the implementation details. If you refactor the implementation, behavior tests shouldn't break!

2. Tests That Depend on Each Other ๐Ÿ”—

Problematic:

private static User _sharedUser;

[Fact]
public void Test1_CreateUser()
{
    _sharedUser = new User { Name = "John" };
    // Test depends on execution order!
}

[Fact]
public void Test2_UpdateUser()
{
    _sharedUser.Name = "Jane"; // Will fail if Test1 hasn't run!
}

Correct:

[Fact]
public void UpdateUser_ChangesName()
{
    // Each test is independent
    var user = new User { Name = "John" };
    user.Name = "Jane";
    Assert.Equal("Jane", user.Name);
}

๐Ÿ’ก Golden Rule: Tests must be independent and run in any order. Use fixtures or setup methods for shared resources.

3. Not Testing Edge Cases ๐ŸŽฏ

Incomplete:

[Fact]
public void Divide_ReturnsQuotient()
{
    Assert.Equal(2, Calculator.Divide(10, 5));
}

Comprehensive:

[Theory]
[InlineData(10, 5, 2)]
[InlineData(10, 3, 3)]  // Integer division
[InlineData(0, 5, 0)]   // Zero dividend
[InlineData(-10, 5, -2)] // Negative numbers
public void Divide_VariousCases_ReturnsQuotient(int x, int y, int expected)
{
    Assert.Equal(expected, Calculator.Divide(x, y));
}

[Fact]
public void Divide_ByZero_ThrowsException()
{
    Assert.Throws<DivideByZeroException>(() => Calculator.Divide(10, 0));
}

๐Ÿง  Think Like an Attacker: What inputs might break your code? Null values? Empty strings? Negative numbers? Maximum values? Test them all!

4. Ignoring Async/Await Properly โฑ๏ธ

Wrong:

[Fact]
public void GetUserAsync_ReturnsUser()
{
    var result = service.GetUserAsync(1).Result; // DON'T USE .Result!
    Assert.NotNull(result);
}

Correct:

[Fact]
public async Task GetUserAsync_ReturnsUser()
{
    var result = await service.GetUserAsync(1);
    Assert.NotNull(result);
}

โš ๏ธ Warning: Using .Result or .Wait() can cause deadlocks in test runners. Always use async/await!

5. Overly Complex Test Setup ๐Ÿ—๏ธ

Too Complex:

[Fact]
public async Task ProcessOrder_ComplexScenario()
{
    // 50 lines of setup...
    var db = CreateDatabase();
    var cache = CreateCache();
    var logger = CreateLogger();
    var emailService = CreateEmailService();
    var paymentGateway = CreatePaymentGateway();
    // ... 20 more dependencies
    
    var service = new OrderService(db, cache, logger, emailService, paymentGateway /*, ... */);
    // What are we even testing here?
}

Simplified:

public class OrderServiceTestFixture
{
    public OrderService CreateService(Action<Mock<IPaymentGateway>>? configurePayment = null)
    {
        var mockPayment = new Mock<IPaymentGateway>();
        configurePayment?.Invoke(mockPayment);
        
        return new OrderService(
            new InMemoryDatabase(),
            mockPayment.Object);
    }
}

[Fact]
public void ProcessOrder_PaymentSucceeds_CompletesOrder()
{
    var service = _fixture.CreateService(payment =>
    {
        payment.Setup(p => p.ChargeAsync(It.IsAny<decimal>()))
               .ReturnsAsync(true);
    });
    
    // Now the test logic is clear!
}

Key Takeaways ๐ŸŽ“

โœ… Test Pyramid: Favor many fast unit tests over few slow E2E tests

โœ… AAA Pattern: Structure tests as Arrange-Act-Assert for clarity

โœ… TDD Workflow: Red (write failing test) โ†’ Green (make it pass) โ†’ Refactor

โœ… Mocking: Use Moq or similar to isolate units from dependencies

โœ… Integration Tests: Use WebApplicationFactory to test complete request pipelines

โœ… Independence: Tests must run in any order without dependencies

โœ… Edge Cases: Test boundary conditions, null values, and error scenarios

โœ… Async/Await: Always use proper async patterns in tests

โœ… Code Coverage: Aim for high coverage, but focus on meaningful tests

โœ… CI/CD Integration: Automate tests to run on every commit

๐Ÿ“š Further Study

  1. Official Microsoft Docs: Unit testing in .NET
  2. xUnit Documentation: xUnit.net Official Docs
  3. Moq Framework: Moq GitHub Repository

๐Ÿ“‹ Quick Reference Card

ConceptKey Points
Unit TestTests single component in isolation; fast, focused
Integration TestTests multiple components together; uses WebApplicationFactory
AAA PatternArrange setup โ†’ Act execution โ†’ Assert verification
MockSimulates dependencies; verify interactions with .Verify()
TheoryParameterized test with [InlineData] for multiple inputs
FixtureIClassFixture for shared setup across tests
TDD CycleRed (fail) โ†’ Green (pass) โ†’ Refactor (improve)
Async TestsUse async Task, never .Result or .Wait()

๐ŸŽฏ Next Steps: Practice writing tests for existing code, then try TDD on your next feature. You'll quickly appreciate how tests serve as living documentation and provide confidence when refactoring!

Practice Questions

Test your understanding with these questions:

Q1: Complete the xUnit attribute for a test method: ```csharp [{{1}}] public void CalculateTotal_ReturnsSum() { Assert.Equal(100, calculator.Total()); } ```
A: ["Fact"]
Q2: What testing framework method creates an in-memory test server for ASP.NET Core integration tests? ```csharp public class ApiTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; public ApiTests(WebApplicationFactory<Program> factory) { _client = factory.{{1}}(); } } ```
A: CreateClient
Q3: Fill in the Moq setup and verification: ```csharp var mock = new {{1}}<IUserRepository>(); mock.{{2}}(r => r.GetByIdAsync(1)) .ReturnsAsync(new User()); // After executing test mock.{{3}}(r => r.SaveAsync(It.IsAny<User>()), Times.Once); ```
A: ["Mock","Setup","Verify"]
Q4: What does this test code verify? ```csharp [Fact] public async Task DeleteUser_NonExistentId_ReturnsNotFound() { var controller = new UsersController(_mockService.Object); var result = await controller.Delete(999); Assert.IsType<NotFoundResult>(result); } ``` A. Controller throws exception for invalid ID B. Controller returns 404 status code for non-existent user C. Controller deletes user with ID 999 D. Service method is called exactly once E. Controller returns 500 error
A: B
Q5: Complete the test using the AAA pattern: ```csharp public void Add_TwoNumbers_ReturnsSum() { // {{1}} - Set up test data var calculator = new Calculator(); var x = 5; var y = 3; // {{2}} - Execute method var result = calculator.Add(x, y); // {{3}} - Verify outcome Assert.Equal(8, result); } ```
A: ["Arrange","Act","Assert"]