学习目标
- 理解测试驱动开发(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#中的单元测试和集成测试,这是现代软件开发中不可或缺的重要技能。
核心概念
测试基础
- xUnit测试框架的使用
- 测试生命周期和Fixture
- 参数化测试和数据驱动测试
- 测试组织和命名约定
Mock和依赖注入测试
- Moq框架的高级用法
- 接口Mock和行为验证
- 依赖注入容器测试
- 复杂依赖关系的处理
异步代码测试
- 异步方法的测试策略
- 异常处理和超时测试
- 取消令牌和流式数据测试
- 并发操作的测试
数据库集成测试
- 内存数据库的使用
- Entity Framework测试
- 事务和数据隔离
- 性能测试和数据验证
高级技术
Web API集成测试
- ASP.NET Core测试主机
- HTTP客户端测试
- 端到端测试流程
- 并发请求处理测试
测试最佳实践
- 测试数据构建器模式
- 自定义断言扩展
- 测试环境隔离
- 性能和资源管理测试
测试驱动开发(TDD)
- 红-绿-重构循环
- 测试优先的开发方法
- 代码质量保证
- 需求驱动的设计
实际应用
购物车系统TDD实现
- 完整的TDD开发流程
- 业务逻辑的测试覆盖
- 边界条件和异常处理
- 代码重构和优化
订单处理系统集成测试
- 完整业务流程测试
- 多服务协作测试
- 数据一致性验证
- 并发处理测试
重要技能
测试策略制定
- 单元测试vs集成测试的选择
- 测试覆盖率的平衡
- 测试维护成本控制
- 持续集成中的测试自动化
质量保证
- 代码质量度量
- 缺陷预防和早期发现
- 回归测试策略
- 测试文档和报告
团队协作
- 测试标准和规范
- 代码审查中的测试要求
- 测试知识分享
- 测试工具和环境管理
通过本章的学习,你已经掌握了现代C#开发中测试的核心技术和最佳实践。这些技能将帮助你编写更可靠、更易维护的代码,并在团队开发中发挥重要作用。
下一章我们将学习部署和DevOps,探讨如何将应用程序部署到生产环境,以及如何建立持续集成和持续部署的流水线。 “`