学习目标

  • 理解测试驱动开发(TDD)的概念和实践
  • 掌握使用xUnit进行单元测试
  • 学会编写集成测试和端到端测试
  • 了解测试覆盖率和质量度量
  • 掌握Mock和Stub的使用
  • 学会测试异步代码和数据库操作

19.1 测试基础和xUnit框架

xUnit测试框架基础

// 基础测试类示例
using Xunit;
using Xunit.Abstractions;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

// 被测试的业务逻辑类
public class Calculator
{
    private readonly ILogger<Calculator> _logger;
    
    public Calculator(ILogger<Calculator> logger = null)
    {
        _logger = logger;
    }
    
    public int Add(int a, int b)
    {
        _logger?.LogInformation("Adding {A} and {B}", a, b);
        return a + b;
    }
    
    public int Subtract(int a, int b)
    {
        _logger?.LogInformation("Subtracting {B} from {A}", b, a);
        return a - b;
    }
    
    public double Divide(double a, double b)
    {
        if (b == 0)
        {
            throw new DivideByZeroException("Cannot divide by zero");
        }
        
        _logger?.LogInformation("Dividing {A} by {B}", a, b);
        return a / b;
    }
    
    public double CalculateAverage(IEnumerable<int> numbers)
    {
        if (numbers == null)
        {
            throw new ArgumentNullException(nameof(numbers));
        }
        
        var numberList = numbers.ToList();
        if (!numberList.Any())
        {
            throw new ArgumentException("Cannot calculate average of empty collection", nameof(numbers));
        }
        
        return numberList.Average();
    }
}

// 基础单元测试类
public class CalculatorTests
{
    private readonly ITestOutputHelper _output;
    private readonly Calculator _calculator;
    
    public CalculatorTests(ITestOutputHelper output)
    {
        _output = output;
        _calculator = new Calculator();
    }
    
    [Fact]
    public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 5;
        int b = 3;
        int expected = 8;
        
        // Act
        int result = _calculator.Add(a, b);
        
        // Assert
        Assert.Equal(expected, result);
        _output.WriteLine($"Adding {a} + {b} = {result}");
    }
    
    [Fact]
    public void Subtract_TwoNumbers_ReturnsCorrectDifference()
    {
        // Arrange
        int a = 10;
        int b = 4;
        int expected = 6;
        
        // Act
        int result = _calculator.Subtract(a, b);
        
        // Assert
        Assert.Equal(expected, result);
    }
    
    [Fact]
    public void Divide_ByZero_ThrowsDivideByZeroException()
    {
        // Arrange
        double a = 10;
        double b = 0;
        
        // Act & Assert
        Assert.Throws<DivideByZeroException>(() => _calculator.Divide(a, b));
    }
    
    [Fact]
    public void Divide_ValidNumbers_ReturnsCorrectQuotient()
    {
        // Arrange
        double a = 15;
        double b = 3;
        double expected = 5;
        
        // Act
        double result = _calculator.Divide(a, b);
        
        // Assert
        Assert.Equal(expected, result, precision: 2);
    }
    
    [Fact]
    public void CalculateAverage_NullCollection_ThrowsArgumentNullException()
    {
        // Arrange
        IEnumerable<int> numbers = null;
        
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => _calculator.CalculateAverage(numbers));
    }
    
    [Fact]
    public void CalculateAverage_EmptyCollection_ThrowsArgumentException()
    {
        // Arrange
        var numbers = new List<int>();
        
        // Act & Assert
        Assert.Throws<ArgumentException>(() => _calculator.CalculateAverage(numbers));
    }
    
    [Fact]
    public void CalculateAverage_ValidNumbers_ReturnsCorrectAverage()
    {
        // Arrange
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        double expected = 3.0;
        
        // Act
        double result = _calculator.CalculateAverage(numbers);
        
        // Assert
        Assert.Equal(expected, result);
    }
}

参数化测试和数据驱动测试

// 参数化测试示例
public class ParameterizedCalculatorTests
{
    private readonly Calculator _calculator;
    
    public ParameterizedCalculatorTests()
    {
        _calculator = new Calculator();
    }
    
    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(0, 5, 5)]
    [InlineData(-1, 1, 0)]
    [InlineData(100, 200, 300)]
    public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
    {
        // Act
        int result = _calculator.Add(a, b);
        
        // Assert
        Assert.Equal(expected, result);
    }
    
    [Theory]
    [InlineData(10, 2, 5)]
    [InlineData(15, 3, 5)]
    [InlineData(20, 4, 5)]
    [InlineData(1, 1, 1)]
    public void Divide_VariousInputs_ReturnsCorrectQuotient(double a, double b, double expected)
    {
        // Act
        double result = _calculator.Divide(a, b);
        
        // Assert
        Assert.Equal(expected, result, precision: 2);
    }
    
    [Theory]
    [MemberData(nameof(GetAverageTestData))]
    public void CalculateAverage_VariousCollections_ReturnsCorrectAverage(
        IEnumerable<int> numbers, 
        double expected)
    {
        // Act
        double result = _calculator.CalculateAverage(numbers);
        
        // Assert
        Assert.Equal(expected, result, precision: 2);
    }
    
    public static IEnumerable<object[]> GetAverageTestData()
    {
        yield return new object[] { new[] { 1, 2, 3 }, 2.0 };
        yield return new object[] { new[] { 10, 20, 30, 40 }, 25.0 };
        yield return new object[] { new[] { 5 }, 5.0 };
        yield return new object[] { new[] { -1, 0, 1 }, 0.0 };
    }
    
    [Theory]
    [ClassData(typeof(DivisionTestData))]
    public void Divide_UsingClassData_ReturnsCorrectResult(double a, double b, double expected)
    {
        // Act
        double result = _calculator.Divide(a, b);
        
        // Assert
        Assert.Equal(expected, result, precision: 2);
    }
}

// 测试数据类
public class DivisionTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 8.0, 2.0, 4.0 };
        yield return new object[] { 15.0, 3.0, 5.0 };
        yield return new object[] { 100.0, 10.0, 10.0 };
        yield return new object[] { 7.0, 2.0, 3.5 };
    }
    
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

测试生命周期和Fixture

// 测试Fixture示例
public class DatabaseFixture : IDisposable
{
    public string ConnectionString { get; private set; }
    public IServiceProvider ServiceProvider { get; private set; }
    
    public DatabaseFixture()
    {
        // 设置测试数据库
        ConnectionString = "Data Source=:memory:";
        
        // 配置依赖注入
        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddConsole());
        services.AddSingleton<Calculator>();
        
        ServiceProvider = services.BuildServiceProvider();
    }
    
    public void Dispose()
    {
        ServiceProvider?.Dispose();
    }
}

// 使用Fixture的测试类
public class CalculatorWithFixtureTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _fixture;
    private readonly Calculator _calculator;
    
    public CalculatorWithFixtureTests(DatabaseFixture fixture)
    {
        _fixture = fixture;
        _calculator = _fixture.ServiceProvider.GetRequiredService<Calculator>();
    }
    
    [Fact]
    public void Add_WithFixture_WorksCorrectly()
    {
        // Act
        int result = _calculator.Add(5, 3);
        
        // Assert
        Assert.Equal(8, result);
    }
}

// 集合Fixture示例
[CollectionDefinition("Calculator Collection")]
public class CalculatorCollection : ICollectionFixture<DatabaseFixture>
{
    // 这个类不需要代码,只是用来定义集合
}

[Collection("Calculator Collection")]
public class CalculatorCollectionTests1
{
    private readonly DatabaseFixture _fixture;
    
    public CalculatorCollectionTests1(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }
    
    [Fact]
    public void Test1()
    {
        // 测试代码
        Assert.True(true);
    }
}

[Collection("Calculator Collection")]
public class CalculatorCollectionTests2
{
    private readonly DatabaseFixture _fixture;
    
    public CalculatorCollectionTests2(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }
    
    [Fact]
    public void Test2()
    {
        // 测试代码
        Assert.True(true);
    }
}

19.2 Mock和依赖注入测试

使用Moq进行Mock测试

using Moq;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

// 业务服务接口
public interface IEmailService
{
    Task<bool> SendEmailAsync(string to, string subject, string body);
    Task<bool> ValidateEmailAsync(string email);
}

public interface IUserRepository
{
    Task<User> GetUserByIdAsync(int id);
    Task<User> GetUserByEmailAsync(string email);
    Task<int> CreateUserAsync(User user);
    Task<bool> UpdateUserAsync(User user);
    Task<bool> DeleteUserAsync(int id);
}

// 用户实体
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsActive { get; set; }
    
    public string FullName => $"{FirstName} {LastName}";
}

// 用户服务
public class UserService
{
    private readonly IUserRepository _userRepository;
    private readonly IEmailService _emailService;
    private readonly ILogger<UserService> _logger;
    
    public UserService(
        IUserRepository userRepository,
        IEmailService emailService,
        ILogger<UserService> logger)
    {
        _userRepository = userRepository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public async Task<User> GetUserAsync(int id)
    {
        _logger.LogInformation("Getting user with ID: {UserId}", id);
        
        if (id <= 0)
        {
            throw new ArgumentException("User ID must be positive", nameof(id));
        }
        
        var user = await _userRepository.GetUserByIdAsync(id);
        if (user == null)
        {
            _logger.LogWarning("User with ID {UserId} not found", id);
        }
        
        return user;
    }
    
    public async Task<int> CreateUserAsync(User user)
    {
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        
        if (string.IsNullOrWhiteSpace(user.Email))
        {
            throw new ArgumentException("Email is required", nameof(user));
        }
        
        // 验证邮箱格式
        var isValidEmail = await _emailService.ValidateEmailAsync(user.Email);
        if (!isValidEmail)
        {
            throw new ArgumentException("Invalid email format", nameof(user));
        }
        
        // 检查邮箱是否已存在
        var existingUser = await _userRepository.GetUserByEmailAsync(user.Email);
        if (existingUser != null)
        {
            throw new InvalidOperationException("User with this email already exists");
        }
        
        user.CreatedAt = DateTime.UtcNow;
        user.IsActive = true;
        
        var userId = await _userRepository.CreateUserAsync(user);
        
        // 发送欢迎邮件
        try
        {
            await _emailService.SendEmailAsync(
                user.Email,
                "Welcome!",
                $"Welcome {user.FullName}! Your account has been created.");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to send welcome email to {Email}", user.Email);
            // 不抛出异常,因为用户创建成功了
        }
        
        _logger.LogInformation("User created successfully with ID: {UserId}", userId);
        return userId;
    }
    
    public async Task<bool> DeactivateUserAsync(int id)
    {
        var user = await _userRepository.GetUserByIdAsync(id);
        if (user == null)
        {
            return false;
        }
        
        user.IsActive = false;
        var result = await _userRepository.UpdateUserAsync(user);
        
        if (result)
        {
            _logger.LogInformation("User {UserId} deactivated successfully", id);
        }
        
        return result;
    }
}

// Mock测试类
public class UserServiceTests
{
    private readonly Mock<IUserRepository> _mockUserRepository;
    private readonly Mock<IEmailService> _mockEmailService;
    private readonly Mock<ILogger<UserService>> _mockLogger;
    private readonly UserService _userService;
    
    public UserServiceTests()
    {
        _mockUserRepository = new Mock<IUserRepository>();
        _mockEmailService = new Mock<IEmailService>();
        _mockLogger = new Mock<ILogger<UserService>>();
        
        _userService = new UserService(
            _mockUserRepository.Object,
            _mockEmailService.Object,
            _mockLogger.Object);
    }
    
    [Fact]
    public async Task GetUserAsync_ValidId_ReturnsUser()
    {
        // Arrange
        int userId = 1;
        var expectedUser = new User
        {
            Id = userId,
            FirstName = "John",
            LastName = "Doe",
            Email = "john.doe@example.com"
        };
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByIdAsync(userId))
            .ReturnsAsync(expectedUser);
        
        // Act
        var result = await _userService.GetUserAsync(userId);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedUser.Id, result.Id);
        Assert.Equal(expectedUser.Email, result.Email);
        
        // 验证方法被调用
        _mockUserRepository.Verify(
            repo => repo.GetUserByIdAsync(userId),
            Times.Once);
    }
    
    [Fact]
    public async Task GetUserAsync_InvalidId_ThrowsArgumentException()
    {
        // Arrange
        int invalidId = -1;
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            () => _userService.GetUserAsync(invalidId));
        
        // 验证repository方法没有被调用
        _mockUserRepository.Verify(
            repo => repo.GetUserByIdAsync(It.IsAny<int>()),
            Times.Never);
    }
    
    [Fact]
    public async Task GetUserAsync_UserNotFound_ReturnsNull()
    {
        // Arrange
        int userId = 999;
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByIdAsync(userId))
            .ReturnsAsync((User)null);
        
        // Act
        var result = await _userService.GetUserAsync(userId);
        
        // Assert
        Assert.Null(result);
    }
    
    [Fact]
    public async Task CreateUserAsync_ValidUser_ReturnsUserId()
    {
        // Arrange
        var user = new User
        {
            FirstName = "Jane",
            LastName = "Smith",
            Email = "jane.smith@example.com"
        };
        
        int expectedUserId = 123;
        
        _mockEmailService
            .Setup(service => service.ValidateEmailAsync(user.Email))
            .ReturnsAsync(true);
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByEmailAsync(user.Email))
            .ReturnsAsync((User)null);
        
        _mockUserRepository
            .Setup(repo => repo.CreateUserAsync(It.IsAny<User>()))
            .ReturnsAsync(expectedUserId);
        
        _mockEmailService
            .Setup(service => service.SendEmailAsync(
                It.IsAny<string>(),
                It.IsAny<string>(),
                It.IsAny<string>()))
            .ReturnsAsync(true);
        
        // Act
        var result = await _userService.CreateUserAsync(user);
        
        // Assert
        Assert.Equal(expectedUserId, result);
        Assert.True(user.IsActive);
        Assert.True(user.CreatedAt > DateTime.MinValue);
        
        // 验证所有依赖方法都被正确调用
        _mockEmailService.Verify(
            service => service.ValidateEmailAsync(user.Email),
            Times.Once);
        
        _mockUserRepository.Verify(
            repo => repo.GetUserByEmailAsync(user.Email),
            Times.Once);
        
        _mockUserRepository.Verify(
            repo => repo.CreateUserAsync(It.Is<User>(u => 
                u.Email == user.Email && 
                u.IsActive == true)),
            Times.Once);
        
        _mockEmailService.Verify(
            service => service.SendEmailAsync(
                user.Email,
                "Welcome!",
                It.IsAny<string>()),
            Times.Once);
    }
    
    [Fact]
    public async Task CreateUserAsync_InvalidEmail_ThrowsArgumentException()
    {
        // Arrange
        var user = new User
        {
            FirstName = "John",
            LastName = "Doe",
            Email = "invalid-email"
        };
        
        _mockEmailService
            .Setup(service => service.ValidateEmailAsync(user.Email))
            .ReturnsAsync(false);
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            () => _userService.CreateUserAsync(user));
        
        // 验证repository方法没有被调用
        _mockUserRepository.Verify(
            repo => repo.CreateUserAsync(It.IsAny<User>()),
            Times.Never);
    }
    
    [Fact]
    public async Task CreateUserAsync_ExistingEmail_ThrowsInvalidOperationException()
    {
        // Arrange
        var user = new User
        {
            FirstName = "John",
            LastName = "Doe",
            Email = "existing@example.com"
        };
        
        var existingUser = new User { Id = 1, Email = user.Email };
        
        _mockEmailService
            .Setup(service => service.ValidateEmailAsync(user.Email))
            .ReturnsAsync(true);
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByEmailAsync(user.Email))
            .ReturnsAsync(existingUser);
        
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _userService.CreateUserAsync(user));
    }
    
    [Fact]
    public async Task CreateUserAsync_EmailSendFails_StillCreatesUser()
    {
        // Arrange
        var user = new User
        {
            FirstName = "John",
            LastName = "Doe",
            Email = "john@example.com"
        };
        
        int expectedUserId = 456;
        
        _mockEmailService
            .Setup(service => service.ValidateEmailAsync(user.Email))
            .ReturnsAsync(true);
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByEmailAsync(user.Email))
            .ReturnsAsync((User)null);
        
        _mockUserRepository
            .Setup(repo => repo.CreateUserAsync(It.IsAny<User>()))
            .ReturnsAsync(expectedUserId);
        
        _mockEmailService
            .Setup(service => service.SendEmailAsync(
                It.IsAny<string>(),
                It.IsAny<string>(),
                It.IsAny<string>()))
            .ThrowsAsync(new Exception("Email service unavailable"));
        
        // Act
        var result = await _userService.CreateUserAsync(user);
        
        // Assert
        Assert.Equal(expectedUserId, result);
        
        // 验证用户仍然被创建
        _mockUserRepository.Verify(
            repo => repo.CreateUserAsync(It.IsAny<User>()),
            Times.Once);
    }
    
    [Fact]
    public async Task DeactivateUserAsync_ExistingUser_ReturnsTrue()
    {
        // Arrange
        int userId = 1;
        var user = new User
        {
            Id = userId,
            FirstName = "John",
            LastName = "Doe",
            Email = "john@example.com",
            IsActive = true
        };
        
        _mockUserRepository
            .Setup(repo => repo.GetUserByIdAsync(userId))
            .ReturnsAsync(user);
        
        _mockUserRepository
            .Setup(repo => repo.UpdateUserAsync(It.IsAny<User>()))
            .ReturnsAsync(true);
        
        // Act
        var result = await _userService.DeactivateUserAsync(userId);
        
        // Assert
        Assert.True(result);
        Assert.False(user.IsActive);
        
        _mockUserRepository.Verify(
            repo => repo.UpdateUserAsync(It.Is<User>(u => u.IsActive == false)),
            Times.Once);
    }
}

高级Mock技术

// 高级Mock技术示例
public class AdvancedMockTests
{
    [Fact]
    public void Mock_WithCallback_CapturesArguments()
    {
        // Arrange
        var mockEmailService = new Mock<IEmailService>();
        string capturedEmail = null;
        string capturedSubject = null;
        
        mockEmailService
            .Setup(service => service.SendEmailAsync(
                It.IsAny<string>(),
                It.IsAny<string>(),
                It.IsAny<string>()))
            .Callback<string, string, string>((email, subject, body) =>
            {
                capturedEmail = email;
                capturedSubject = subject;
            })
            .ReturnsAsync(true);
        
        // Act
        var service = mockEmailService.Object;
        service.SendEmailAsync("test@example.com", "Test Subject", "Test Body");
        
        // Assert
        Assert.Equal("test@example.com", capturedEmail);
        Assert.Equal("Test Subject", capturedSubject);
    }
    
    [Fact]
    public void Mock_WithSequentialReturns_ReturnsInOrder()
    {
        // Arrange
        var mockRepository = new Mock<IUserRepository>();
        
        mockRepository
            .SetupSequence(repo => repo.GetUserByIdAsync(1))
            .ReturnsAsync(new User { Id = 1, FirstName = "First" })
            .ReturnsAsync(new User { Id = 1, FirstName = "Second" })
            .ReturnsAsync((User)null);
        
        // Act & Assert
        var service = mockRepository.Object;
        
        var first = service.GetUserByIdAsync(1).Result;
        Assert.Equal("First", first.FirstName);
        
        var second = service.GetUserByIdAsync(1).Result;
        Assert.Equal("Second", second.FirstName);
        
        var third = service.GetUserByIdAsync(1).Result;
        Assert.Null(third);
    }
    
    [Fact]
    public void Mock_WithConditionalSetup_ReturnsBasedOnCondition()
    {
        // Arrange
        var mockRepository = new Mock<IUserRepository>();
        
        mockRepository
            .Setup(repo => repo.GetUserByIdAsync(It.Is<int>(id => id > 0)))
            .ReturnsAsync(new User { Id = 1 });
        
        mockRepository
            .Setup(repo => repo.GetUserByIdAsync(It.Is<int>(id => id <= 0)))
            .ThrowsAsync(new ArgumentException("Invalid ID"));
        
        // Act & Assert
        var service = mockRepository.Object;
        
        var validResult = service.GetUserByIdAsync(1).Result;
        Assert.NotNull(validResult);
        
        Assert.ThrowsAsync<ArgumentException>(() => service.GetUserByIdAsync(-1));
    }
    
    [Fact]
    public void Mock_VerifyWithTimes_ChecksCallCount()
    {
        // Arrange
        var mockRepository = new Mock<IUserRepository>();
        mockRepository
            .Setup(repo => repo.GetUserByIdAsync(It.IsAny<int>()))
            .ReturnsAsync(new User());
        
        var service = mockRepository.Object;
        
        // Act
        service.GetUserByIdAsync(1);
        service.GetUserByIdAsync(2);
        service.GetUserByIdAsync(3);
        
        // Assert
        mockRepository.Verify(
            repo => repo.GetUserByIdAsync(It.IsAny<int>()),
            Times.Exactly(3));
        
        mockRepository.Verify(
            repo => repo.GetUserByIdAsync(1),
            Times.Once);
        
        mockRepository.Verify(
            repo => repo.GetUserByIdAsync(4),
            Times.Never);
    }
    
    [Fact]
    public void Mock_WithStrictBehavior_ThrowsOnUnexpectedCalls()
    {
        // Arrange
        var mockRepository = new Mock<IUserRepository>(MockBehavior.Strict);
        
        mockRepository
            .Setup(repo => repo.GetUserByIdAsync(1))
            .ReturnsAsync(new User { Id = 1 });
        
        var service = mockRepository.Object;
        
        // Act & Assert
        var result = service.GetUserByIdAsync(1).Result;
        Assert.NotNull(result);
        
        // 这会抛出异常,因为没有设置对ID=2的调用
        Assert.ThrowsAsync<MockException>(() => service.GetUserByIdAsync(2));
    }
}

19.3 异步代码测试

异步方法测试

// 异步服务示例
public interface IAsyncDataService
{
    Task<string> GetDataAsync(int id);
    Task<List<string>> GetMultipleDataAsync(IEnumerable<int> ids);
    Task ProcessDataAsync(string data);
    Task<bool> TryProcessDataAsync(string data);
    IAsyncEnumerable<string> GetDataStreamAsync();
}

public class AsyncDataService : IAsyncDataService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<AsyncDataService> _logger;
    
    public AsyncDataService(HttpClient httpClient, ILogger<AsyncDataService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }
    
    public async Task<string> GetDataAsync(int id)
    {
        if (id <= 0)
        {
            throw new ArgumentException("ID must be positive", nameof(id));
        }
        
        try
        {
            _logger.LogInformation("Fetching data for ID: {Id}", id);
            
            var response = await _httpClient.GetAsync($"/api/data/{id}");
            response.EnsureSuccessStatusCode();
            
            var content = await response.Content.ReadAsStringAsync();
            return content;
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to fetch data for ID: {Id}", id);
            throw new InvalidOperationException($"Failed to fetch data for ID {id}", ex);
        }
    }
    
    public async Task<List<string>> GetMultipleDataAsync(IEnumerable<int> ids)
    {
        if (ids == null)
        {
            throw new ArgumentNullException(nameof(ids));
        }
        
        var tasks = ids.Select(id => GetDataAsync(id));
        var results = await Task.WhenAll(tasks);
        
        return results.ToList();
    }
    
    public async Task ProcessDataAsync(string data)
    {
        if (string.IsNullOrWhiteSpace(data))
        {
            throw new ArgumentException("Data cannot be empty", nameof(data));
        }
        
        _logger.LogInformation("Processing data: {Data}", data);
        
        // 模拟异步处理
        await Task.Delay(100);
        
        // 模拟可能的处理失败
        if (data.Contains("error"))
        {
            throw new InvalidOperationException("Processing failed");
        }
        
        _logger.LogInformation("Data processed successfully");
    }
    
    public async Task<bool> TryProcessDataAsync(string data)
    {
        try
        {
            await ProcessDataAsync(data);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to process data: {Data}", data);
            return false;
        }
    }
    
    public async IAsyncEnumerable<string> GetDataStreamAsync()
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(50); // 模拟异步操作
            yield return $"Data item {i}";
        }
    }
}

// 异步测试类
public class AsyncDataServiceTests
{
    private readonly Mock<HttpClient> _mockHttpClient;
    private readonly Mock<ILogger<AsyncDataService>> _mockLogger;
    private readonly AsyncDataService _service;
    
    public AsyncDataServiceTests()
    {
        _mockHttpClient = new Mock<HttpClient>();
        _mockLogger = new Mock<ILogger<AsyncDataService>>();
        _service = new AsyncDataService(_mockHttpClient.Object, _mockLogger.Object);
    }
    
    [Fact]
    public async Task GetDataAsync_ValidId_ReturnsData()
    {
        // Arrange
        int id = 1;
        string expectedData = "test data";
        
        var mockResponse = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectedData)
        };
        
        var mockHandler = new Mock<HttpMessageHandler>();
        mockHandler
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(mockResponse);
        
        var httpClient = new HttpClient(mockHandler.Object)
        {
            BaseAddress = new Uri("https://api.example.com")
        };
        
        var service = new AsyncDataService(httpClient, _mockLogger.Object);
        
        // Act
        var result = await service.GetDataAsync(id);
        
        // Assert
        Assert.Equal(expectedData, result);
    }
    
    [Fact]
    public async Task GetDataAsync_InvalidId_ThrowsArgumentException()
    {
        // Arrange
        int invalidId = -1;
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            () => _service.GetDataAsync(invalidId));
    }
    
    [Fact]
    public async Task GetDataAsync_HttpRequestFails_ThrowsInvalidOperationException()
    {
        // Arrange
        int id = 1;
        
        var mockHandler = new Mock<HttpMessageHandler>();
        mockHandler
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ThrowsAsync(new HttpRequestException("Network error"));
        
        var httpClient = new HttpClient(mockHandler.Object);
        var service = new AsyncDataService(httpClient, _mockLogger.Object);
        
        // Act & Assert
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => service.GetDataAsync(id));
        
        Assert.Contains("Failed to fetch data", exception.Message);
        Assert.IsType<HttpRequestException>(exception.InnerException);
    }
    
    [Fact]
    public async Task GetMultipleDataAsync_ValidIds_ReturnsAllData()
    {
        // Arrange
        var ids = new[] { 1, 2, 3 };
        var expectedData = new[] { "data1", "data2", "data3" };
        
        var mockHandler = new Mock<HttpMessageHandler>();
        
        for (int i = 0; i < ids.Length; i++)
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(expectedData[i])
            };
            
            mockHandler
                .Protected()
                .Setup<Task<HttpResponseMessage>>(
                    "SendAsync",
                    ItExpr.Is<HttpRequestMessage>(req => 
                        req.RequestUri.ToString().Contains($"/api/data/{ids[i]}")),
                    ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(response);
        }
        
        var httpClient = new HttpClient(mockHandler.Object)
        {
            BaseAddress = new Uri("https://api.example.com")
        };
        
        var service = new AsyncDataService(httpClient, _mockLogger.Object);
        
        // Act
        var results = await service.GetMultipleDataAsync(ids);
        
        // Assert
        Assert.Equal(expectedData.Length, results.Count);
        for (int i = 0; i < expectedData.Length; i++)
        {
            Assert.Equal(expectedData[i], results[i]);
        }
    }
    
    [Fact]
    public async Task ProcessDataAsync_ValidData_CompletesSuccessfully()
    {
        // Arrange
        string data = "valid data";
        
        // Act
        await _service.ProcessDataAsync(data);
        
        // Assert - 如果没有抛出异常,测试通过
        Assert.True(true);
    }
    
    [Fact]
    public async Task ProcessDataAsync_DataWithError_ThrowsInvalidOperationException()
    {
        // Arrange
        string data = "data with error";
        
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _service.ProcessDataAsync(data));
    }
    
    [Fact]
    public async Task TryProcessDataAsync_ValidData_ReturnsTrue()
    {
        // Arrange
        string data = "valid data";
        
        // Act
        var result = await _service.TryProcessDataAsync(data);
        
        // Assert
        Assert.True(result);
    }
    
    [Fact]
    public async Task TryProcessDataAsync_InvalidData_ReturnsFalse()
    {
        // Arrange
        string data = "data with error";
        
        // Act
        var result = await _service.TryProcessDataAsync(data);
        
        // Assert
        Assert.False(result);
    }
    
    [Fact]
    public async Task GetDataStreamAsync_ReturnsExpectedItems()
    {
        // Arrange
        var expectedItems = new[] { "Data item 1", "Data item 2", "Data item 3", "Data item 4", "Data item 5" };
        var actualItems = new List<string>();
        
        // Act
        await foreach (var item in _service.GetDataStreamAsync())
        {
            actualItems.Add(item);
        }
        
        // Assert
        Assert.Equal(expectedItems.Length, actualItems.Count);
        for (int i = 0; i < expectedItems.Length; i++)
        {
            Assert.Equal(expectedItems[i], actualItems[i]);
        }
    }
    
    [Fact]
    public async Task GetDataStreamAsync_WithCancellation_StopsEarly()
    {
        // Arrange
        var cts = new CancellationTokenSource();
        var actualItems = new List<string>();
        
        // Act
        try
        {
            await foreach (var item in _service.GetDataStreamAsync().WithCancellation(cts.Token))
            {
                actualItems.Add(item);
                if (actualItems.Count == 2)
                {
                    cts.Cancel(); // 取消操作
                }
            }
        }
        catch (OperationCanceledException)
        {
            // 预期的异常
        }
        
        // Assert
        Assert.Equal(2, actualItems.Count);
    }
}

// 异步测试的超时和取消
public class AsyncTimeoutTests
{
    [Fact(Timeout = 5000)] // 5秒超时
    public async Task LongRunningOperation_CompletesWithinTimeout()
    {
        // Arrange
        var service = new AsyncDataService(new HttpClient(), Mock.Of<ILogger<AsyncDataService>>());
        
        // Act
        await service.ProcessDataAsync("test data");
        
        // Assert
        Assert.True(true);
    }
    
    [Fact]
    public async Task CancellableOperation_RespectsCancellationToken()
    {
        // Arrange
        var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        
        // Act & Assert
        await Assert.ThrowsAsync<OperationCanceledException>(async () =>
        {
            await Task.Delay(1000, cts.Token);
        });
    }
    
    [Fact]
    public async Task AsyncOperation_WithCustomTimeout_ThrowsTimeoutException()
    {
        // Arrange
        var timeout = TimeSpan.FromMilliseconds(100);
        
        // Act & Assert
        await Assert.ThrowsAsync<TimeoutException>(async () =>
        {
            using var cts = new CancellationTokenSource(timeout);
            try
            {
                await Task.Delay(1000, cts.Token);
            }
            catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
            {
                throw new TimeoutException("Operation timed out");
            }
        });
    }
}

19.4 数据库集成测试

Entity Framework Core测试

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Data.Sqlite;

// 测试用的DbContext
public class TestDbContext : DbContext
{
    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
    {
    }
    
    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Email).IsRequired().HasMaxLength(255);
            entity.HasIndex(e => e.Email).IsUnique();
        });
        
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Total).HasColumnType("decimal(18,2)");
            entity.HasOne(e => e.User)
                  .WithMany(u => u.Orders)
                  .HasForeignKey(e => e.UserId);
        });
    }
}

// 订单实体
public class Order
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public decimal Total { get; set; }
    public DateTime OrderDate { get; set; }
    public string Status { get; set; }
    
    public User User { get; set; }
}

// 扩展User实体
public partial class User
{
    public List<Order> Orders { get; set; } = new();
}

// 数据库服务
public class DatabaseUserService
{
    private readonly TestDbContext _context;
    private readonly ILogger<DatabaseUserService> _logger;
    
    public DatabaseUserService(TestDbContext context, ILogger<DatabaseUserService> logger)
    {
        _context = context;
        _logger = logger;
    }
    
    public async Task<User> CreateUserAsync(User user)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        
        if (await _context.Users.AnyAsync(u => u.Email == user.Email))
            throw new InvalidOperationException("User with this email already exists");
        
        user.CreatedAt = DateTime.UtcNow;
        user.IsActive = true;
        
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        
        _logger.LogInformation("User created with ID: {UserId}", user.Id);
        return user;
    }
    
    public async Task<User> GetUserWithOrdersAsync(int userId)
    {
        return await _context.Users
            .Include(u => u.Orders)
            .FirstOrDefaultAsync(u => u.Id == userId);
    }
    
    public async Task<Order> CreateOrderAsync(int userId, decimal total)
    {
        var user = await _context.Users.FindAsync(userId);
        if (user == null)
            throw new ArgumentException("User not found", nameof(userId));
        
        var order = new Order
        {
            UserId = userId,
            Total = total,
            OrderDate = DateTime.UtcNow,
            Status = "Pending"
        };
        
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
        
        return order;
    }
    
    public async Task<List<User>> GetUsersWithOrdersAboveAmountAsync(decimal amount)
    {
        return await _context.Users
            .Include(u => u.Orders)
            .Where(u => u.Orders.Any(o => o.Total > amount))
            .ToListAsync();
    }
    
    public async Task<bool> DeleteUserAsync(int userId)
    {
        var user = await _context.Users
            .Include(u => u.Orders)
            .FirstOrDefaultAsync(u => u.Id == userId);
        
        if (user == null)
            return false;
        
        // 删除相关订单
        _context.Orders.RemoveRange(user.Orders);
        _context.Users.Remove(user);
        
        await _context.SaveChangesAsync();
        return true;
    }
}

// 数据库测试基类
public abstract class DatabaseTestBase : IDisposable
{
    protected TestDbContext Context { get; private set; }
    protected DatabaseUserService Service { get; private set; }
    private SqliteConnection _connection;
    
    protected DatabaseTestBase()
    {
        // 创建内存数据库连接
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();
        
        // 配置DbContext选项
        var options = new DbContextOptionsBuilder<TestDbContext>()
            .UseSqlite(_connection)
            .EnableSensitiveDataLogging()
            .Options;
        
        Context = new TestDbContext(options);
        Context.Database.EnsureCreated();
        
        // 创建服务
        var logger = Mock.Of<ILogger<DatabaseUserService>>();
        Service = new DatabaseUserService(Context, logger);
    }
    
    protected async Task SeedDataAsync()
    {
        var users = new[]
        {
            new User { FirstName = "John", LastName = "Doe", Email = "john@example.com" },
            new User { FirstName = "Jane", LastName = "Smith", Email = "jane@example.com" },
            new User { FirstName = "Bob", LastName = "Johnson", Email = "bob@example.com" }
        };
        
        Context.Users.AddRange(users);
        await Context.SaveChangesAsync();
        
        var orders = new[]
        {
            new Order { UserId = 1, Total = 100.50m, OrderDate = DateTime.UtcNow.AddDays(-5), Status = "Completed" },
            new Order { UserId = 1, Total = 250.75m, OrderDate = DateTime.UtcNow.AddDays(-2), Status = "Pending" },
            new Order { UserId = 2, Total = 75.25m, OrderDate = DateTime.UtcNow.AddDays(-1), Status = "Completed" },
            new Order { UserId = 3, Total = 500.00m, OrderDate = DateTime.UtcNow, Status = "Processing" }
        };
        
        Context.Orders.AddRange(orders);
        await Context.SaveChangesAsync();
    }
    
    public void Dispose()
    {
        Context?.Dispose();
        _connection?.Dispose();
    }
}

// 数据库集成测试
public class DatabaseUserServiceTests : DatabaseTestBase
{
    [Fact]
    public async Task CreateUserAsync_ValidUser_CreatesUserInDatabase()
    {
        // Arrange
        var user = new User
        {
            FirstName = "Test",
            LastName = "User",
            Email = "test@example.com"
        };
        
        // Act
        var result = await Service.CreateUserAsync(user);
        
        // Assert
        Assert.True(result.Id > 0);
        Assert.Equal(user.Email, result.Email);
        Assert.True(result.IsActive);
        Assert.True(result.CreatedAt > DateTime.MinValue);
        
        // 验证数据库中确实存在该用户
        var dbUser = await Context.Users.FindAsync(result.Id);
        Assert.NotNull(dbUser);
        Assert.Equal(user.Email, dbUser.Email);
    }
    
    [Fact]
    public async Task CreateUserAsync_DuplicateEmail_ThrowsInvalidOperationException()
    {
        // Arrange
        await SeedDataAsync();
        
        var user = new User
        {
            FirstName = "Duplicate",
            LastName = "User",
            Email = "john@example.com" // 已存在的邮箱
        };
        
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => Service.CreateUserAsync(user));
    }
    
    [Fact]
    public async Task GetUserWithOrdersAsync_ExistingUser_ReturnsUserWithOrders()
    {
        // Arrange
        await SeedDataAsync();
        int userId = 1;
        
        // Act
        var result = await Service.GetUserWithOrdersAsync(userId);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(userId, result.Id);
        Assert.NotEmpty(result.Orders);
        Assert.Equal(2, result.Orders.Count); // John有2个订单
    }
    
    [Fact]
    public async Task CreateOrderAsync_ValidUser_CreatesOrder()
    {
        // Arrange
        await SeedDataAsync();
        int userId = 1;
        decimal total = 150.00m;
        
        // Act
        var result = await Service.CreateOrderAsync(userId, total);
        
        // Assert
        Assert.True(result.Id > 0);
        Assert.Equal(userId, result.UserId);
        Assert.Equal(total, result.Total);
        Assert.Equal("Pending", result.Status);
        
        // 验证数据库中的订单
        var dbOrder = await Context.Orders.FindAsync(result.Id);
        Assert.NotNull(dbOrder);
        Assert.Equal(total, dbOrder.Total);
    }
    
    [Fact]
    public async Task CreateOrderAsync_NonExistentUser_ThrowsArgumentException()
    {
        // Arrange
        int nonExistentUserId = 999;
        decimal total = 100.00m;
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            () => Service.CreateOrderAsync(nonExistentUserId, total));
    }
    
    [Fact]
    public async Task GetUsersWithOrdersAboveAmountAsync_ValidAmount_ReturnsFilteredUsers()
    {
        // Arrange
        await SeedDataAsync();
        decimal amount = 200.00m;
        
        // Act
        var result = await Service.GetUsersWithOrdersAboveAmountAsync(amount);
        
        // Assert
        Assert.NotEmpty(result);
        Assert.Equal(2, result.Count); // John (250.75) 和 Bob (500.00)
        
        var userIds = result.Select(u => u.Id).ToList();
        Assert.Contains(1, userIds); // John
        Assert.Contains(3, userIds); // Bob
        Assert.DoesNotContain(2, userIds); // Jane (最高75.25)
    }
    
    [Fact]
    public async Task DeleteUserAsync_ExistingUser_DeletesUserAndOrders()
    {
        // Arrange
        await SeedDataAsync();
        int userId = 1;
        
        // 验证用户和订单存在
        var userBefore = await Context.Users.Include(u => u.Orders).FirstAsync(u => u.Id == userId);
        Assert.NotNull(userBefore);
        Assert.NotEmpty(userBefore.Orders);
        
        // Act
        var result = await Service.DeleteUserAsync(userId);
        
        // Assert
        Assert.True(result);
        
        // 验证用户已删除
        var userAfter = await Context.Users.FindAsync(userId);
        Assert.Null(userAfter);
        
        // 验证相关订单也已删除
        var ordersAfter = await Context.Orders.Where(o => o.UserId == userId).ToListAsync();
        Assert.Empty(ordersAfter);
    }
    
    [Fact]
    public async Task DeleteUserAsync_NonExistentUser_ReturnsFalse()
    {
        // Arrange
        int nonExistentUserId = 999;
        
        // Act
        var result = await Service.DeleteUserAsync(nonExistentUserId);
        
        // Assert
        Assert.False(result);
    }
    
    [Fact]
    public async Task DatabaseOperations_WithTransaction_MaintainsConsistency()
    {
        // Arrange
        await SeedDataAsync();
        
        using var transaction = await Context.Database.BeginTransactionAsync();
        
        try
        {
            // Act - 在事务中执行多个操作
            var user = new User
            {
                FirstName = "Transaction",
                LastName = "Test",
                Email = "transaction@example.com"
            };
            
            var createdUser = await Service.CreateUserAsync(user);
            var order = await Service.CreateOrderAsync(createdUser.Id, 300.00m);
            
            // 验证事务中的数据
            var userInTransaction = await Service.GetUserWithOrdersAsync(createdUser.Id);
            Assert.NotNull(userInTransaction);
            Assert.Single(userInTransaction.Orders);
            
            // 回滚事务
            await transaction.RollbackAsync();
            
            // Assert - 验证回滚后数据不存在
            var userAfterRollback = await Context.Users.FindAsync(createdUser.Id);
            Assert.Null(userAfterRollback);
            
            var orderAfterRollback = await Context.Orders.FindAsync(order.Id);
            Assert.Null(orderAfterRollback);
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

// 数据库性能测试
public class DatabasePerformanceTests : DatabaseTestBase
{
    [Fact]
    public async Task BulkInsert_LargeNumberOfUsers_CompletesInReasonableTime()
    {
        // Arrange
        const int userCount = 1000;
        var users = Enumerable.Range(1, userCount)
            .Select(i => new User
            {
                FirstName = $"User{i}",
                LastName = "Test",
                Email = $"user{i}@example.com",
                CreatedAt = DateTime.UtcNow,
                IsActive = true
            })
            .ToList();
        
        var stopwatch = Stopwatch.StartNew();
        
        // Act
        Context.Users.AddRange(users);
        await Context.SaveChangesAsync();
        
        stopwatch.Stop();
        
        // Assert
        Assert.True(stopwatch.ElapsedMilliseconds < 5000, 
            $"Bulk insert took {stopwatch.ElapsedMilliseconds}ms, expected < 5000ms");
        
        var count = await Context.Users.CountAsync();
        Assert.Equal(userCount, count);
    }
    
    [Fact]
    public async Task ComplexQuery_WithIncludes_ExecutesEfficiently()
    {
        // Arrange
        await SeedDataAsync();
        var stopwatch = Stopwatch.StartNew();
        
        // Act
        var result = await Context.Users
            .Include(u => u.Orders)
            .Where(u => u.IsActive)
            .Where(u => u.Orders.Any(o => o.Total > 100))
            .OrderBy(u => u.LastName)
            .ToListAsync();
        
        stopwatch.Stop();
        
        // Assert
        Assert.True(stopwatch.ElapsedMilliseconds < 1000,
            $"Complex query took {stopwatch.ElapsedMilliseconds}ms, expected < 1000ms");
        
        Assert.NotEmpty(result);
     }
 }

19.5 Web API集成测试

ASP.NET Core测试

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using System.Net.Http.Json;
using System.Text.Json;

// Web API控制器
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly DatabaseUserService _userService;
    private readonly ILogger<UsersController> _logger;
    
    public UsersController(DatabaseUserService userService, ILogger<UsersController> logger)
    {
        _userService = userService;
        _logger = logger;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        try
        {
            var user = await _userService.GetUserWithOrdersAsync(id);
            if (user == null)
            {
                return NotFound($"User with ID {id} not found");
            }
            
            var userDto = new UserDto
            {
                Id = user.Id,
                FirstName = user.FirstName,
                LastName = user.LastName,
                Email = user.Email,
                IsActive = user.IsActive,
                OrderCount = user.Orders?.Count ?? 0
            };
            
            return Ok(userDto);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting user {UserId}", id);
            return StatusCode(500, "Internal server error");
        }
    }
    
    [HttpPost]
    public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        
        try
        {
            var user = new User
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.Email
            };
            
            var createdUser = await _userService.CreateUserAsync(user);
            
            var userDto = new UserDto
            {
                Id = createdUser.Id,
                FirstName = createdUser.FirstName,
                LastName = createdUser.LastName,
                Email = createdUser.Email,
                IsActive = createdUser.IsActive,
                OrderCount = 0
            };
            
            return CreatedAtAction(nameof(GetUser), new { id = userDto.Id }, userDto);
        }
        catch (InvalidOperationException ex)
        {
            return Conflict(ex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating user");
            return StatusCode(500, "Internal server error");
        }
    }
    
    [HttpDelete("{id}")]
    public async Task<ActionResult> DeleteUser(int id)
    {
        try
        {
            var result = await _userService.DeleteUserAsync(id);
            if (!result)
            {
                return NotFound($"User with ID {id} not found");
            }
            
            return NoContent();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error deleting user {UserId}", id);
            return StatusCode(500, "Internal server error");
        }
    }
    
    [HttpGet]
    public async Task<ActionResult<List<UserDto>>> GetUsersWithHighValueOrders([FromQuery] decimal minAmount = 100)
    {
        try
        {
            var users = await _userService.GetUsersWithOrdersAboveAmountAsync(minAmount);
            
            var userDtos = users.Select(u => new UserDto
            {
                Id = u.Id,
                FirstName = u.FirstName,
                LastName = u.LastName,
                Email = u.Email,
                IsActive = u.IsActive,
                OrderCount = u.Orders?.Count ?? 0
            }).ToList();
            
            return Ok(userDtos);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting users with high value orders");
            return StatusCode(500, "Internal server error");
        }
    }
}

// DTO类
public class UserDto
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public bool IsActive { get; set; }
    public int OrderCount { get; set; }
}

public class CreateUserRequest
{
    [Required]
    [StringLength(50)]
    public string FirstName { get; set; }
    
    [Required]
    [StringLength(50)]
    public string LastName { get; set; }
    
    [Required]
    [EmailAddress]
    [StringLength(255)]
    public string Email { get; set; }
}

// 测试用的Startup类
public class TestStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 配置内存数据库
        services.AddDbContext<TestDbContext>(options =>
            options.UseInMemoryDatabase("TestDatabase"));
        
        services.AddScoped<DatabaseUserService>();
        services.AddControllers();
        services.AddLogging();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

// 自定义WebApplicationFactory
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
    where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 移除现有的DbContext注册
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<TestDbContext>));
            if (descriptor != null)
            {
                services.Remove(descriptor);
            }
            
            // 添加内存数据库
            services.AddDbContext<TestDbContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
            });
            
            // 确保数据库被创建
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<TestDbContext>();
            db.Database.EnsureCreated();
        });
    }
}

// Web API集成测试
public class UsersControllerIntegrationTests : IClassFixture<CustomWebApplicationFactory<TestStartup>>
{
    private readonly CustomWebApplicationFactory<TestStartup> _factory;
    private readonly HttpClient _client;
    
    public UsersControllerIntegrationTests(CustomWebApplicationFactory<TestStartup> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
    }
    
    [Fact]
    public async Task GetUser_ExistingUser_ReturnsUserDto()
    {
        // Arrange
        await SeedTestDataAsync();
        int userId = 1;
        
        // Act
        var response = await _client.GetAsync($"/api/users/{userId}");
        
        // Assert
        response.EnsureSuccessStatusCode();
        
        var userDto = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(userDto);
        Assert.Equal(userId, userDto.Id);
        Assert.Equal("John", userDto.FirstName);
        Assert.Equal("Doe", userDto.LastName);
        Assert.True(userDto.OrderCount > 0);
    }
    
    [Fact]
    public async Task GetUser_NonExistentUser_ReturnsNotFound()
    {
        // Arrange
        int nonExistentUserId = 999;
        
        // Act
        var response = await _client.GetAsync($"/api/users/{nonExistentUserId}");
        
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("not found", content);
    }
    
    [Fact]
    public async Task CreateUser_ValidRequest_ReturnsCreatedUser()
    {
        // Arrange
        var request = new CreateUserRequest
        {
            FirstName = "New",
            LastName = "User",
            Email = "newuser@example.com"
        };
        
        // Act
        var response = await _client.PostAsJsonAsync("/api/users", request);
        
        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        
        var userDto = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(userDto);
        Assert.True(userDto.Id > 0);
        Assert.Equal(request.FirstName, userDto.FirstName);
        Assert.Equal(request.LastName, userDto.LastName);
        Assert.Equal(request.Email, userDto.Email);
        Assert.True(userDto.IsActive);
        
        // 验证Location头
        Assert.NotNull(response.Headers.Location);
        Assert.Contains($"/api/users/{userDto.Id}", response.Headers.Location.ToString());
    }
    
    [Fact]
    public async Task CreateUser_InvalidEmail_ReturnsBadRequest()
    {
        // Arrange
        var request = new CreateUserRequest
        {
            FirstName = "Invalid",
            LastName = "User",
            Email = "invalid-email" // 无效邮箱格式
        };
        
        // Act
        var response = await _client.PostAsJsonAsync("/api/users", request);
        
        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
    
    [Fact]
    public async Task CreateUser_DuplicateEmail_ReturnsConflict()
    {
        // Arrange
        await SeedTestDataAsync();
        
        var request = new CreateUserRequest
        {
            FirstName = "Duplicate",
            LastName = "User",
            Email = "john@example.com" // 已存在的邮箱
        };
        
        // Act
        var response = await _client.PostAsJsonAsync("/api/users", request);
        
        // Assert
        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
    }
    
    [Fact]
    public async Task DeleteUser_ExistingUser_ReturnsNoContent()
    {
        // Arrange
        await SeedTestDataAsync();
        int userId = 1;
        
        // Act
        var response = await _client.DeleteAsync($"/api/users/{userId}");
        
        // Assert
        Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
        
        // 验证用户已被删除
        var getResponse = await _client.GetAsync($"/api/users/{userId}");
        Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
    }
    
    [Fact]
    public async Task DeleteUser_NonExistentUser_ReturnsNotFound()
    {
        // Arrange
        int nonExistentUserId = 999;
        
        // Act
        var response = await _client.DeleteAsync($"/api/users/{nonExistentUserId}");
        
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
    
    [Fact]
    public async Task GetUsersWithHighValueOrders_WithMinAmount_ReturnsFilteredUsers()
    {
        // Arrange
        await SeedTestDataAsync();
        decimal minAmount = 200;
        
        // Act
        var response = await _client.GetAsync($"/api/users?minAmount={minAmount}");
        
        // Assert
        response.EnsureSuccessStatusCode();
        
        var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
        Assert.NotNull(users);
        Assert.NotEmpty(users);
        
        // 验证返回的用户确实有高价值订单
        foreach (var user in users)
        {
            Assert.True(user.OrderCount > 0);
        }
    }
    
    [Fact]
    public async Task GetUsersWithHighValueOrders_DefaultMinAmount_ReturnsUsersWithOrdersAbove100()
    {
        // Arrange
        await SeedTestDataAsync();
        
        // Act
        var response = await _client.GetAsync("/api/users");
        
        // Assert
        response.EnsureSuccessStatusCode();
        
        var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
        Assert.NotNull(users);
        // 应该返回有订单金额超过100的用户
    }
    
    [Theory]
    [InlineData("application/json")]
    [InlineData("application/xml")]
    public async Task CreateUser_DifferentContentTypes_HandledCorrectly(string contentType)
    {
        // Arrange
        var request = new CreateUserRequest
        {
            FirstName = "Content",
            LastName = "Test",
            Email = "content@example.com"
        };
        
        // Act
        HttpResponseMessage response;
        if (contentType == "application/json")
        {
            response = await _client.PostAsJsonAsync("/api/users", request);
        }
        else
        {
            // 对于XML,这里简化处理,实际项目中需要配置XML序列化
            response = await _client.PostAsJsonAsync("/api/users", request);
        }
        
        // Assert
        if (contentType == "application/json")
        {
            Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        }
    }
    
    [Fact]
    public async Task ApiEndpoints_ConcurrentRequests_HandleCorrectly()
    {
        // Arrange
        await SeedTestDataAsync();
        var tasks = new List<Task<HttpResponseMessage>>();
        
        // Act - 发送并发请求
        for (int i = 0; i < 10; i++)
        {
            tasks.Add(_client.GetAsync("/api/users/1"));
        }
        
        var responses = await Task.WhenAll(tasks);
        
        // Assert
        foreach (var response in responses)
        {
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }
    }
    
    private async Task SeedTestDataAsync()
    {
        using var scope = _factory.Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<TestDbContext>();
        
        // 清理现有数据
        context.Orders.RemoveRange(context.Orders);
        context.Users.RemoveRange(context.Users);
        await context.SaveChangesAsync();
        
        // 添加测试数据
        var users = new[]
        {
            new User { FirstName = "John", LastName = "Doe", Email = "john@example.com", IsActive = true, CreatedAt = DateTime.UtcNow },
            new User { FirstName = "Jane", LastName = "Smith", Email = "jane@example.com", IsActive = true, CreatedAt = DateTime.UtcNow },
            new User { FirstName = "Bob", LastName = "Johnson", Email = "bob@example.com", IsActive = true, CreatedAt = DateTime.UtcNow }
        };
        
        context.Users.AddRange(users);
        await context.SaveChangesAsync();
        
        var orders = new[]
        {
            new Order { UserId = 1, Total = 100.50m, OrderDate = DateTime.UtcNow.AddDays(-5), Status = "Completed" },
            new Order { UserId = 1, Total = 250.75m, OrderDate = DateTime.UtcNow.AddDays(-2), Status = "Pending" },
            new Order { UserId = 2, Total = 75.25m, OrderDate = DateTime.UtcNow.AddDays(-1), Status = "Completed" },
            new Order { UserId = 3, Total = 500.00m, OrderDate = DateTime.UtcNow, Status = "Processing" }
        };
        
        context.Orders.AddRange(orders);
        await context.SaveChangesAsync();
    }
}

19.6 测试最佳实践

测试组织和命名

// 测试命名约定:MethodName_Scenario_ExpectedResult
public class CalculatorTestsWithBestPractices
{
    private readonly Calculator _calculator;
    
    public CalculatorTestsWithBestPractices()
    {
        _calculator = new Calculator();
    }
    
    [Fact]
    public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 5;
        int b = 3;
        int expected = 8;
        
        // Act
        int result = _calculator.Add(a, b);
        
        // Assert
        Assert.Equal(expected, result);
    }
    
    [Fact]
    public void Divide_ByZero_ThrowsDivideByZeroException()
    {
        // Arrange
        int dividend = 10;
        int divisor = 0;
        
        // Act & Assert
        Assert.Throws<DivideByZeroException>(() => _calculator.Divide(dividend, divisor));
    }
    
    [Theory]
    [InlineData(0, 0, 0)]
    [InlineData(1, 0, 1)]
    [InlineData(0, 1, 1)]
    [InlineData(-1, 1, 0)]
    [InlineData(int.MaxValue, 1, int.MinValue)] // 溢出测试
    public void Add_VariousInputs_ReturnsExpectedResults(int a, int b, int expected)
    {
        // Act
        int result = _calculator.Add(a, b);
        
        // Assert
        Assert.Equal(expected, result);
    }
}

// 测试数据构建器模式
public class UserTestDataBuilder
{
    private User _user;
    
    public UserTestDataBuilder()
    {
        _user = new User
        {
            FirstName = "Default",
            LastName = "User",
            Email = "default@example.com",
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };
    }
    
    public UserTestDataBuilder WithFirstName(string firstName)
    {
        _user.FirstName = firstName;
        return this;
    }
    
    public UserTestDataBuilder WithLastName(string lastName)
    {
        _user.LastName = lastName;
        return this;
    }
    
    public UserTestDataBuilder WithEmail(string email)
    {
        _user.Email = email;
        return this;
    }
    
    public UserTestDataBuilder WithInactiveStatus()
    {
        _user.IsActive = false;
        return this;
    }
    
    public UserTestDataBuilder WithCreatedDate(DateTime createdAt)
    {
        _user.CreatedAt = createdAt;
        return this;
    }
    
    public UserTestDataBuilder WithOrders(params Order[] orders)
    {
        _user.Orders = orders.ToList();
        return this;
    }
    
    public User Build() => _user;
    
    public static implicit operator User(UserTestDataBuilder builder) => builder.Build();
}

// 使用测试数据构建器
public class UserServiceTestsWithBuilder
{
    private readonly Mock<IUserRepository> _mockRepository;
    private readonly UserService _service;
    
    public UserServiceTestsWithBuilder()
    {
        _mockRepository = new Mock<IUserRepository>();
        _service = new UserService(_mockRepository.Object);
    }
    
    [Fact]
    public async Task GetActiveUsersAsync_WithActiveUsers_ReturnsOnlyActiveUsers()
    {
        // Arrange
        var activeUser = new UserTestDataBuilder()
            .WithFirstName("Active")
            .WithEmail("active@example.com")
            .Build();
            
        var inactiveUser = new UserTestDataBuilder()
            .WithFirstName("Inactive")
            .WithEmail("inactive@example.com")
            .WithInactiveStatus()
            .Build();
            
        var users = new[] { activeUser, inactiveUser };
        
        _mockRepository
            .Setup(r => r.GetAllAsync())
            .ReturnsAsync(users);
        
        // Act
        var result = await _service.GetActiveUsersAsync();
        
        // Assert
        Assert.Single(result);
        Assert.Equal("Active", result.First().FirstName);
    }
    
    [Fact]
    public async Task GetUserWithHighValueOrdersAsync_UserWithHighValueOrders_ReturnsUser()
    {
        // Arrange
        var highValueOrder = new Order { Total = 1000m, Status = "Completed" };
        var lowValueOrder = new Order { Total = 50m, Status = "Completed" };
        
        var userWithHighValueOrders = new UserTestDataBuilder()
            .WithFirstName("HighValue")
            .WithEmail("highvalue@example.com")
            .WithOrders(highValueOrder, lowValueOrder)
            .Build();
            
        _mockRepository
            .Setup(r => r.GetByIdAsync(It.IsAny<int>()))
            .ReturnsAsync(userWithHighValueOrders);
        
        // Act
        var result = await _service.GetUserWithHighValueOrdersAsync(1, 500m);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal("HighValue", result.FirstName);
    }
}

// 自定义断言扩展
public static class CustomAssertions
{
    public static void ShouldBeEquivalentTo<T>(this T actual, T expected, string because = "")
    {
        // 使用反射比较对象属性
        var properties = typeof(T).GetProperties();
        
        foreach (var property in properties)
        {
            var actualValue = property.GetValue(actual);
            var expectedValue = property.GetValue(expected);
            
            Assert.True(
                Equals(actualValue, expectedValue),
                $"Property {property.Name} does not match. Expected: {expectedValue}, Actual: {actualValue}. {because}");
        }
    }
    
    public static void ShouldContainUser(this IEnumerable<User> users, string email, string because = "")
    {
        Assert.True(
            users.Any(u => u.Email == email),
            $"Collection should contain user with email '{email}'. {because}");
    }
    
    public static void ShouldHaveCount<T>(this IEnumerable<T> collection, int expectedCount, string because = "")
    {
        var actualCount = collection.Count();
        Assert.True(
            actualCount == expectedCount,
            $"Collection should have {expectedCount} items but has {actualCount}. {because}");
    }
    
    public static void ShouldBeInRange(this decimal actual, decimal min, decimal max, string because = "")
    {
        Assert.True(
            actual >= min && actual <= max,
            $"Value {actual} should be between {min} and {max}. {because}");
    }
}

// 使用自定义断言
public class UserServiceTestsWithCustomAssertions
{
    [Fact]
    public async Task GetUsersAsync_ReturnsExpectedUsers()
    {
        // Arrange
        var expectedUser = new UserTestDataBuilder()
            .WithFirstName("Test")
            .WithLastName("User")
            .WithEmail("test@example.com")
            .Build();
            
        var service = new UserService(Mock.Of<IUserRepository>());
        
        // Act
        var users = new[] { expectedUser };
        
        // Assert - 使用自定义断言
        users.ShouldHaveCount(1, "because we added one user");
        users.ShouldContainUser("test@example.com", "because we added this user");
        
        var actualUser = users.First();
        actualUser.ShouldBeEquivalentTo(expectedUser, "because they should be the same");
    }
}

// 测试性能和资源管理
public class PerformanceTests
{
    [Fact]
    public void LargeDataProcessing_CompletesWithinTimeLimit()
    {
        // Arrange
        var data = Enumerable.Range(1, 100000).ToList();
        var processor = new DataProcessor();
        var stopwatch = Stopwatch.StartNew();
        
        // Act
        var result = processor.ProcessLargeDataSet(data);
        stopwatch.Stop();
        
        // Assert
        Assert.True(stopwatch.ElapsedMilliseconds < 1000, 
            $"Processing took {stopwatch.ElapsedMilliseconds}ms, expected < 1000ms");
        Assert.NotNull(result);
    }
    
    [Fact]
    public void MemoryIntensiveOperation_DoesNotExceedMemoryLimit()
    {
        // Arrange
        var initialMemory = GC.GetTotalMemory(true);
        var processor = new DataProcessor();
        
        // Act
        processor.MemoryIntensiveOperation();
        
        // Force garbage collection
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        
        var finalMemory = GC.GetTotalMemory(true);
        var memoryIncrease = finalMemory - initialMemory;
        
        // Assert
        Assert.True(memoryIncrease < 50 * 1024 * 1024, // 50MB limit
            $"Memory increase was {memoryIncrease / (1024 * 1024)}MB, expected < 50MB");
    }
}

// 测试环境隔离
public class IsolatedTestEnvironment : IDisposable
{
    private readonly string _tempDirectory;
    private readonly TestDbContext _context;
    
    public IsolatedTestEnvironment()
    {
        // 创建临时目录
        _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(_tempDirectory);
        
        // 创建独立的数据库上下文
        var options = new DbContextOptionsBuilder<TestDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
            
        _context = new TestDbContext(options);
        _context.Database.EnsureCreated();
    }
    
    public TestDbContext GetDbContext() => _context;
    
    public string GetTempDirectory() => _tempDirectory;
    
    public void Dispose()
    {
        _context?.Dispose();
        
        if (Directory.Exists(_tempDirectory))
        {
            Directory.Delete(_tempDirectory, true);
        }
    }
}

// 使用隔离测试环境
public class IsolatedTests
{
    [Fact]
    public async Task FileOperation_InIsolatedEnvironment_DoesNotAffectOtherTests()
    {
        // Arrange
        using var environment = new IsolatedTestEnvironment();
        var tempDir = environment.GetTempDirectory();
        var filePath = Path.Combine(tempDir, "test.txt");
        
        // Act
        await File.WriteAllTextAsync(filePath, "test content");
        var content = await File.ReadAllTextAsync(filePath);
        
        // Assert
        Assert.Equal("test content", content);
        Assert.True(File.Exists(filePath));
        
        // 环境会在Dispose时自动清理
    }
    
    [Fact]
    public async Task DatabaseOperation_InIsolatedEnvironment_DoesNotAffectOtherTests()
    {
        // Arrange
        using var environment = new IsolatedTestEnvironment();
        var context = environment.GetDbContext();
        
        var user = new User
        {
            FirstName = "Isolated",
            LastName = "Test",
            Email = "isolated@example.com",
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };
        
        // Act
        context.Users.Add(user);
        await context.SaveChangesAsync();
        
        var savedUser = await context.Users.FirstAsync();
        
        // Assert
        Assert.Equal("Isolated", savedUser.FirstName);
        
        // 数据库会在Dispose时自动清理
     }
 }

19.7 实践练习

练习1:测试驱动开发(TDD)

// 需求:实现一个购物车类,支持添加商品、移除商品、计算总价、应用折扣

// 第一步:编写测试
public class ShoppingCartTests
{
    [Fact]
    public void AddItem_NewItem_IncreasesItemCount()
    {
        // Arrange
        var cart = new ShoppingCart();
        var item = new CartItem("Product1", 10.00m, 2);
        
        // Act
        cart.AddItem(item);
        
        // Assert
        Assert.Equal(1, cart.ItemCount);
        Assert.Equal(2, cart.GetQuantity("Product1"));
    }
    
    [Fact]
    public void AddItem_ExistingItem_UpdatesQuantity()
    {
        // Arrange
        var cart = new ShoppingCart();
        var item1 = new CartItem("Product1", 10.00m, 2);
        var item2 = new CartItem("Product1", 10.00m, 3);
        
        // Act
        cart.AddItem(item1);
        cart.AddItem(item2);
        
        // Assert
        Assert.Equal(1, cart.ItemCount);
        Assert.Equal(5, cart.GetQuantity("Product1"));
    }
    
    [Fact]
    public void RemoveItem_ExistingItem_DecreasesQuantity()
    {
        // Arrange
        var cart = new ShoppingCart();
        var item = new CartItem("Product1", 10.00m, 5);
        cart.AddItem(item);
        
        // Act
        cart.RemoveItem("Product1", 2);
        
        // Assert
        Assert.Equal(3, cart.GetQuantity("Product1"));
    }
    
    [Fact]
    public void RemoveItem_AllQuantity_RemovesItemCompletely()
    {
        // Arrange
        var cart = new ShoppingCart();
        var item = new CartItem("Product1", 10.00m, 3);
        cart.AddItem(item);
        
        // Act
        cart.RemoveItem("Product1", 3);
        
        // Assert
        Assert.Equal(0, cart.ItemCount);
        Assert.Equal(0, cart.GetQuantity("Product1"));
    }
    
    [Fact]
    public void CalculateTotal_WithItems_ReturnsCorrectTotal()
    {
        // Arrange
        var cart = new ShoppingCart();
        cart.AddItem(new CartItem("Product1", 10.00m, 2)); // 20.00
        cart.AddItem(new CartItem("Product2", 15.50m, 1)); // 15.50
        
        // Act
        var total = cart.CalculateTotal();
        
        // Assert
        Assert.Equal(35.50m, total);
    }
    
    [Fact]
    public void ApplyDiscount_ValidPercentage_ReducesTotal()
    {
        // Arrange
        var cart = new ShoppingCart();
        cart.AddItem(new CartItem("Product1", 100.00m, 1));
        
        // Act
        cart.ApplyDiscount(0.10m); // 10% discount
        var total = cart.CalculateTotal();
        
        // Assert
        Assert.Equal(90.00m, total);
    }
    
    [Theory]
    [InlineData(-0.1)]
    [InlineData(1.1)]
    [InlineData(2.0)]
    public void ApplyDiscount_InvalidPercentage_ThrowsArgumentException(decimal discount)
    {
        // Arrange
        var cart = new ShoppingCart();
        
        // Act & Assert
        Assert.Throws<ArgumentException>(() => cart.ApplyDiscount(discount));
    }
    
    [Fact]
    public void Clear_WithItems_RemovesAllItems()
    {
        // Arrange
        var cart = new ShoppingCart();
        cart.AddItem(new CartItem("Product1", 10.00m, 2));
        cart.AddItem(new CartItem("Product2", 15.50m, 1));
        
        // Act
        cart.Clear();
        
        // Assert
        Assert.Equal(0, cart.ItemCount);
        Assert.Equal(0, cart.CalculateTotal());
    }
}

// 第二步:实现购物车类(让测试通过)
public class CartItem
{
    public string ProductName { get; }
    public decimal Price { get; }
    public int Quantity { get; set; }
    
    public CartItem(string productName, decimal price, int quantity)
    {
        if (string.IsNullOrWhiteSpace(productName))
            throw new ArgumentException("Product name cannot be empty", nameof(productName));
        if (price < 0)
            throw new ArgumentException("Price cannot be negative", nameof(price));
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive", nameof(quantity));
            
        ProductName = productName;
        Price = price;
        Quantity = quantity;
    }
    
    public decimal GetTotal() => Price * Quantity;
}

public class ShoppingCart
{
    private readonly Dictionary<string, CartItem> _items;
    private decimal _discountPercentage;
    
    public ShoppingCart()
    {
        _items = new Dictionary<string, CartItem>();
        _discountPercentage = 0;
    }
    
    public int ItemCount => _items.Count;
    
    public void AddItem(CartItem item)
    {
        if (item == null)
            throw new ArgumentNullException(nameof(item));
            
        if (_items.ContainsKey(item.ProductName))
        {
            _items[item.ProductName].Quantity += item.Quantity;
        }
        else
        {
            _items[item.ProductName] = new CartItem(item.ProductName, item.Price, item.Quantity);
        }
    }
    
    public void RemoveItem(string productName, int quantity)
    {
        if (string.IsNullOrWhiteSpace(productName))
            throw new ArgumentException("Product name cannot be empty", nameof(productName));
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive", nameof(quantity));
            
        if (_items.ContainsKey(productName))
        {
            var item = _items[productName];
            if (item.Quantity <= quantity)
            {
                _items.Remove(productName);
            }
            else
            {
                item.Quantity -= quantity;
            }
        }
    }
    
    public int GetQuantity(string productName)
    {
        return _items.ContainsKey(productName) ? _items[productName].Quantity : 0;
    }
    
    public decimal CalculateTotal()
    {
        var subtotal = _items.Values.Sum(item => item.GetTotal());
        return subtotal * (1 - _discountPercentage);
    }
    
    public void ApplyDiscount(decimal discountPercentage)
    {
        if (discountPercentage < 0 || discountPercentage > 1)
            throw new ArgumentException("Discount percentage must be between 0 and 1", nameof(discountPercentage));
            
        _discountPercentage = discountPercentage;
    }
    
    public void Clear()
    {
        _items.Clear();
        _discountPercentage = 0;
    }
    
    public IReadOnlyCollection<CartItem> GetItems()
    {
        return _items.Values.ToList().AsReadOnly();
    }
}

练习2:集成测试项目

// 订单处理系统集成测试
public class OrderProcessingIntegrationTests : IClassFixture<CustomWebApplicationFactory<TestStartup>>
{
    private readonly CustomWebApplicationFactory<TestStartup> _factory;
    private readonly HttpClient _client;
    
    public OrderProcessingIntegrationTests(CustomWebApplicationFactory<TestStartup> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
    }
    
    [Fact]
    public async Task CompleteOrderFlow_FromCreationToCompletion_WorksCorrectly()
    {
        // Arrange - 创建用户
        var createUserRequest = new CreateUserRequest
        {
            FirstName = "Integration",
            LastName = "Test",
            Email = "integration@example.com"
        };
        
        var userResponse = await _client.PostAsJsonAsync("/api/users", createUserRequest);
        userResponse.EnsureSuccessStatusCode();
        var user = await userResponse.Content.ReadFromJsonAsync<UserDto>();
        
        // Act 1 - 创建订单
        var createOrderRequest = new CreateOrderRequest
        {
            UserId = user.Id,
            Items = new[]
            {
                new OrderItemRequest { ProductName = "Product1", Price = 10.00m, Quantity = 2 },
                new OrderItemRequest { ProductName = "Product2", Price = 15.50m, Quantity = 1 }
            }
        };
        
        var orderResponse = await _client.PostAsJsonAsync("/api/orders", createOrderRequest);
        orderResponse.EnsureSuccessStatusCode();
        var order = await orderResponse.Content.ReadFromJsonAsync<OrderDto>();
        
        // Assert 1 - 验证订单创建
        Assert.NotNull(order);
        Assert.Equal(user.Id, order.UserId);
        Assert.Equal(35.50m, order.Total);
        Assert.Equal("Pending", order.Status);
        
        // Act 2 - 更新订单状态
        var updateStatusRequest = new UpdateOrderStatusRequest
        {
            Status = "Processing"
        };
        
        var updateResponse = await _client.PutAsJsonAsync($"/api/orders/{order.Id}/status", updateStatusRequest);
        updateResponse.EnsureSuccessStatusCode();
        
        // Act 3 - 获取更新后的订单
        var getOrderResponse = await _client.GetAsync($"/api/orders/{order.Id}");
        getOrderResponse.EnsureSuccessStatusCode();
        var updatedOrder = await getOrderResponse.Content.ReadFromJsonAsync<OrderDto>();
        
        // Assert 2 - 验证订单状态更新
        Assert.Equal("Processing", updatedOrder.Status);
        
        // Act 4 - 完成订单
        var completeRequest = new UpdateOrderStatusRequest
        {
            Status = "Completed"
        };
        
        var completeResponse = await _client.PutAsJsonAsync($"/api/orders/{order.Id}/status", completeRequest);
        completeResponse.EnsureSuccessStatusCode();
        
        // Act 5 - 获取用户的所有订单
        var userOrdersResponse = await _client.GetAsync($"/api/users/{user.Id}/orders");
        userOrdersResponse.EnsureSuccessStatusCode();
        var userOrders = await userOrdersResponse.Content.ReadFromJsonAsync<List<OrderDto>>();
        
        // Assert 3 - 验证完整流程
        Assert.Single(userOrders);
        Assert.Equal("Completed", userOrders.First().Status);
        Assert.Equal(order.Id, userOrders.First().Id);
    }
    
    [Fact]
    public async Task OrderValidation_InvalidData_ReturnsValidationErrors()
    {
        // Arrange
        var invalidOrderRequest = new CreateOrderRequest
        {
            UserId = 999, // 不存在的用户
            Items = new[]
            {
                new OrderItemRequest { ProductName = "", Price = -10.00m, Quantity = 0 } // 无效数据
            }
        };
        
        // Act
        var response = await _client.PostAsJsonAsync("/api/orders", invalidOrderRequest);
        
        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        
        var errorContent = await response.Content.ReadAsStringAsync();
        Assert.Contains("validation", errorContent.ToLower());
    }
    
    [Fact]
    public async Task ConcurrentOrderCreation_MultipleUsers_HandledCorrectly()
    {
        // Arrange
        var tasks = new List<Task<HttpResponseMessage>>();
        
        // 创建多个用户和订单的并发请求
        for (int i = 0; i < 5; i++)
        {
            var userRequest = new CreateUserRequest
            {
                FirstName = $"User{i}",
                LastName = "Test",
                Email = $"user{i}@example.com"
            };
            
            tasks.Add(_client.PostAsJsonAsync("/api/users", userRequest));
        }
        
        // Act
        var responses = await Task.WhenAll(tasks);
        
        // Assert
        foreach (var response in responses)
        {
            Assert.True(response.IsSuccessStatusCode, 
                $"Response failed with status: {response.StatusCode}");
        }
        
        // 验证所有用户都被创建
        var users = new List<UserDto>();
        foreach (var response in responses)
        {
            var user = await response.Content.ReadFromJsonAsync<UserDto>();
            users.Add(user);
        }
        
        Assert.Equal(5, users.Count);
        Assert.Equal(5, users.Select(u => u.Email).Distinct().Count()); // 确保邮箱唯一
    }
}

// 支持的DTO和请求类
public class OrderDto
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public decimal Total { get; set; }
    public string Status { get; set; }
    public DateTime OrderDate { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class OrderItemDto
{
    public string ProductName { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public decimal Total { get; set; }
}

public class CreateOrderRequest
{
    [Required]
    public int UserId { get; set; }
    
    [Required]
    [MinLength(1)]
    public OrderItemRequest[] Items { get; set; }
}

public class OrderItemRequest
{
    [Required]
    [StringLength(100)]
    public string ProductName { get; set; }
    
    [Range(0.01, double.MaxValue)]
    public decimal Price { get; set; }
    
    [Range(1, int.MaxValue)]
    public int Quantity { get; set; }
}

public class UpdateOrderStatusRequest
{
    [Required]
    [StringLength(50)]
    public string Status { get; set; }
}

19.8 练习演示

// 综合测试演示
public class TestingDemonstration
{
    public static async Task RunAllTestsDemo()
    {
        Console.WriteLine("=== C# 单元测试和集成测试演示 ===");
        
        // 1. 单元测试演示
        Console.WriteLine("\n1. 单元测试演示:");
        await RunUnitTestsDemo();
        
        // 2. Mock测试演示
        Console.WriteLine("\n2. Mock测试演示:");
        await RunMockTestsDemo();
        
        // 3. 数据库集成测试演示
        Console.WriteLine("\n3. 数据库集成测试演示:");
        await RunDatabaseTestsDemo();
        
        // 4. Web API集成测试演示
        Console.WriteLine("\n4. Web API集成测试演示:");
        await RunWebApiTestsDemo();
        
        // 5. TDD演示
        Console.WriteLine("\n5. 测试驱动开发演示:");
        RunTddDemo();
        
        Console.WriteLine("\n=== 测试演示完成 ===");
    }
    
    private static async Task RunUnitTestsDemo()
    {
        try
        {
            var calculator = new Calculator();
            
            // 基本运算测试
            var addResult = calculator.Add(5, 3);
            Console.WriteLine($"加法测试: 5 + 3 = {addResult} (期望: 8)");
            
            var multiplyResult = calculator.Multiply(4, 6);
            Console.WriteLine($"乘法测试: 4 * 6 = {multiplyResult} (期望: 24)");
            
            // 异常测试
            try
            {
                calculator.Divide(10, 0);
            }
            catch (DivideByZeroException)
            {
                Console.WriteLine("除零异常测试: 通过");
            }
            
            Console.WriteLine("✓ 单元测试演示完成");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"✗ 单元测试演示失败: {ex.Message}");
        }
    }
    
    private static async Task RunMockTestsDemo()
    {
        try
        {
            // 创建Mock对象
            var mockEmailService = new Mock<IEmailService>();
            var mockUserRepository = new Mock<IUserRepository>();
            
            // 设置Mock行为
            mockEmailService
                .Setup(s => s.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
                .ReturnsAsync(true);
                
            var testUser = new User
            {
                Id = 1,
                FirstName = "Test",
                LastName = "User",
                Email = "test@example.com",
                IsActive = true
            };
            
            mockUserRepository
                .Setup(r => r.GetByIdAsync(1))
                .ReturnsAsync(testUser);
            
            // 测试服务
            var userService = new UserService(mockUserRepository.Object, mockEmailService.Object);
            var user = await userService.GetUserAsync(1);
            
            Console.WriteLine($"Mock测试: 获取用户 {user.FirstName} {user.LastName}");
            
            // 验证Mock调用
            mockUserRepository.Verify(r => r.GetByIdAsync(1), Times.Once);
            Console.WriteLine("✓ Mock验证通过");
            
            Console.WriteLine("✓ Mock测试演示完成");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"✗ Mock测试演示失败: {ex.Message}");
        }
    }
    
    private static async Task RunDatabaseTestsDemo()
    {
        try
        {
            // 使用内存数据库进行测试
            var options = new DbContextOptionsBuilder<TestDbContext>()
                .UseInMemoryDatabase("TestDemo")
                .Options;
                
            using var context = new TestDbContext(options);
            var service = new DatabaseUserService(context);
            
            // 创建测试用户
            var user = new User
            {
                FirstName = "Database",
                LastName = "Test",
                Email = "dbtest@example.com",
                IsActive = true,
                CreatedAt = DateTime.UtcNow
            };
            
            var createdUser = await service.CreateUserAsync(user);
            Console.WriteLine($"数据库测试: 创建用户 {createdUser.FirstName} (ID: {createdUser.Id})");
            
            // 查询用户
            var retrievedUser = await service.GetUserWithOrdersAsync(createdUser.Id);
            Console.WriteLine($"数据库测试: 查询用户 {retrievedUser.FirstName}");
            
            Console.WriteLine("✓ 数据库集成测试演示完成");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"✗ 数据库集成测试演示失败: {ex.Message}");
        }
    }
    
    private static async Task RunWebApiTestsDemo()
    {
        try
        {
            Console.WriteLine("Web API集成测试需要启动测试服务器");
            Console.WriteLine("- 测试HTTP GET/POST/PUT/DELETE请求");
            Console.WriteLine("- 验证响应状态码和内容");
            Console.WriteLine("- 测试并发请求处理");
            Console.WriteLine("- 验证数据验证和错误处理");
            Console.WriteLine("✓ Web API集成测试概念演示完成");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"✗ Web API集成测试演示失败: {ex.Message}");
        }
    }
    
    private static void RunTddDemo()
    {
        try
        {
            Console.WriteLine("TDD演示 - 购物车功能:");
            
            // 1. 红色阶段:编写失败的测试
            Console.WriteLine("1. 红色阶段: 编写测试(测试失败)");
            
            // 2. 绿色阶段:编写最少代码让测试通过
            Console.WriteLine("2. 绿色阶段: 实现功能(测试通过)");
            
            var cart = new ShoppingCart();
            cart.AddItem(new CartItem("Product1", 10.00m, 2));
            cart.AddItem(new CartItem("Product2", 15.50m, 1));
            
            Console.WriteLine($"   - 添加商品后,购物车商品数量: {cart.ItemCount}");
            Console.WriteLine($"   - 购物车总价: {cart.CalculateTotal():C}");
            
            // 3. 重构阶段:优化代码
            Console.WriteLine("3. 重构阶段: 优化代码结构");
            
            cart.ApplyDiscount(0.1m); // 10% 折扣
            Console.WriteLine($"   - 应用10%折扣后总价: {cart.CalculateTotal():C}");
            
            Console.WriteLine("✓ TDD演示完成");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"✗ TDD演示失败: {ex.Message}");
        }
    }
}

本章总结

在本章中,我们深入学习了C#中的单元测试和集成测试,这是现代软件开发中不可或缺的重要技能。

核心概念

  1. 测试基础

    • xUnit测试框架的使用
    • 测试生命周期和Fixture
    • 参数化测试和数据驱动测试
    • 测试组织和命名约定
  2. Mock和依赖注入测试

    • Moq框架的高级用法
    • 接口Mock和行为验证
    • 依赖注入容器测试
    • 复杂依赖关系的处理
  3. 异步代码测试

    • 异步方法的测试策略
    • 异常处理和超时测试
    • 取消令牌和流式数据测试
    • 并发操作的测试
  4. 数据库集成测试

    • 内存数据库的使用
    • Entity Framework测试
    • 事务和数据隔离
    • 性能测试和数据验证

高级技术

  1. Web API集成测试

    • ASP.NET Core测试主机
    • HTTP客户端测试
    • 端到端测试流程
    • 并发请求处理测试
  2. 测试最佳实践

    • 测试数据构建器模式
    • 自定义断言扩展
    • 测试环境隔离
    • 性能和资源管理测试
  3. 测试驱动开发(TDD)

    • 红-绿-重构循环
    • 测试优先的开发方法
    • 代码质量保证
    • 需求驱动的设计

实际应用

  1. 购物车系统TDD实现

    • 完整的TDD开发流程
    • 业务逻辑的测试覆盖
    • 边界条件和异常处理
    • 代码重构和优化
  2. 订单处理系统集成测试

    • 完整业务流程测试
    • 多服务协作测试
    • 数据一致性验证
    • 并发处理测试

重要技能

  1. 测试策略制定

    • 单元测试vs集成测试的选择
    • 测试覆盖率的平衡
    • 测试维护成本控制
    • 持续集成中的测试自动化
  2. 质量保证

    • 代码质量度量
    • 缺陷预防和早期发现
    • 回归测试策略
    • 测试文档和报告
  3. 团队协作

    • 测试标准和规范
    • 代码审查中的测试要求
    • 测试知识分享
    • 测试工具和环境管理

通过本章的学习,你已经掌握了现代C#开发中测试的核心技术和最佳实践。这些技能将帮助你编写更可靠、更易维护的代码,并在团队开发中发挥重要作用。

下一章我们将学习部署和DevOps,探讨如何将应用程序部署到生产环境,以及如何建立持续集成和持续部署的流水线。 “`