C#代码测试全指南
一、测试基础概念
1. 测试类型分类
测试类型 | 目的 | 范围 | 执行频率 |
---|---|---|---|
单元测试 | 验证单个代码单元功能 | 类/方法级别 | 开发阶段持续执行 |
集成测试 | 验证模块间交互 | 模块/服务级别 | 集成阶段执行 |
功能测试 | 验证业务功能 | 端到端级别 | 手动/自动化执行 |
性能测试 | 评估系统性能指标 | 系统级别 | 发布前执行 |
压力测试 | 测试系统极限承载能力 | 系统级别 | 发布前执行 |
回归测试 | 验证变更未引入新缺陷 | 全量/增量 | 每次变更后执行 |
2. 测试金字塔
┌─────────────┐
│ 手动测试 │
└─────────────┘
▲
│
▼
┌───────────────────┐
│ 端到端测试 │
└───────────────────┘
▲
│
▼
┌───────────────────────┐
│ 集成测试 │
└───────────────────────┘
▲
│
▼
┌─────────────────────────┐
│ 单元测试 │
└─────────────────────────┘
二、单元测试实现
1. 基础单元测试框架
使用xUnit框架示例:
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
Assert.Equal(expected, calculator.Add(a, b));
}
}
测试类设计原则:
- 测试类命名:
[被测类名]Tests
- 测试方法命名:
[动作]_[条件]_[预期结果]
- 每个测试方法包含Arrange-Act-Assert三部分
2. Mock与依赖注入
使用Moq框架模拟依赖:
using Moq;
using Xunit;
public class OrderServiceTests
{
[Fact]
public void PlaceOrder_ValidOrder_CallsRepository()
{
// Arrange
var mockRepo = new Mock<IOrderRepository>();
var service = new OrderService(mockRepo.Object);
var order = new Order { Id = 1 };
// Act
service.PlaceOrder(order);
// Assert
mockRepo.Verify(repo => repo.Save(order), Times.Once);
}
}
依赖注入测试模式:
public interface IDataService
{
string GetData();
}
public class RealDataService : IDataService
{
public string GetData() => "Real Data";
}
public class MockDataService : IDataService
{
public string GetData() => "Mock Data";
}
public class Consumer
{
private readonly IDataService _dataService;
public Consumer(IDataService dataService)
{
_dataService = dataService;
}
public string Process() => _dataService.GetData();
}
// 测试中使用
var mockService = new MockDataService();
var consumer = new Consumer(mockService);
Assert.Equal("Mock Data", consumer.Process());
3. 测试覆盖率
使用Visual Studio测试资源管理器:
- 右键项目 → "分析" → "计算代码覆盖率"
- 查看未覆盖的代码区域
- 补充测试用例
提高覆盖率技巧:
- 测试正常路径
- 测试边界条件
- 测试异常情况
- 测试私有方法(通过公共接口间接测试)
三、性能测试实现
1. 基础性能测试
使用Stopwatch测量执行时间:
using System.Diagnostics;
public class PerformanceTests
{
[Fact]
public void ProcessData_PerformanceTest()
{
// Arrange
var data = Enumerable.Range(1, 10000).ToList();
var processor = new DataProcessor();
// Act & Assert
var sw = Stopwatch.StartNew();
processor.Process(data);
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < 100); // 100ms内完成
}
}
2. 压力测试框架
使用Apache JMeter或k6:
- 创建测试计划
- 配置虚拟用户数
- 设置请求速率
- 监控响应时间/错误率
C#压力测试示例(使用HttpClient):
using System.Net.Http;
using System.Threading.Tasks;
public class ApiStressTests
{
private readonly HttpClient _client = new HttpClient();
[Fact]
public async Task ApiEndpoint_HandleConcurrentRequests()
{
// Arrange
int concurrentRequests = 100;
var tasks = new List<Task>();
// Act
for (int i = 0; i < concurrentRequests; i++)
{
tasks.Add(MakeRequest());
}
await Task.WhenAll(tasks);
// Assert
// 检查是否有失败请求
}
private async Task MakeRequest()
{
var response = await _client.GetAsync("https://api.example.com/data");
response.EnsureSuccessStatusCode();
}
}
3. 性能计数器监控
使用System.Diagnostics.PerformanceCounter:
using System.Diagnostics;
public class PerformanceMonitor
{
public void MonitorCpuUsage()
{
var counter = new PerformanceCounter(
categoryName: "Processor",
counterName: "% Processor Time",
instanceName: "_Total");
float cpuUsage = counter.NextValue();
System.Threading.Thread.Sleep(1000); // 需要等待1秒
cpuUsage = counter.NextValue();
Console.WriteLine($"CPU使用率: {cpuUsage}%");
}
}
四、测试最佳实践
1. 测试驱动开发(TDD)
TDD循环:
- 编写失败的测试
- 编写最小实现通过测试
- 重构代码
示例:
// 第一步:编写测试
[Fact]
public void Calculator_ShouldAddNumbers()
{
var calc = new Calculator();
Assert.Equal(4, calc.Add(2, 2)); // 测试失败
}
// 第二步:实现功能
public class Calculator
{
public int Add(int a, int b) => a + b; // 测试通过
}
// 第三步:重构(如有必要)
2. 测试数据管理
使用测试数据生成器:
public static class TestData
{
public static IEnumerable<object[]> AdditionData()
{
yield return new object[] { 1, 1, 2 };
yield return new object[] { 0, 0, 0 };
yield return new object[] { -1, 1, 0 };
yield return new object[] { int.MaxValue, 0, int.MaxValue };
}
[Theory]
[MemberData(nameof(AdditionData))]
public void Add_ValidInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calc = new Calculator();
Assert.Equal(expected, calc.Add(a, b));
}
}
3. 测试隔离
避免测试间依赖:
// 不好的做法 - 测试间共享状态
public class BadTests
{
private static int _sharedValue;
[Fact]
public void Test1()
{
_sharedValue = 1;
Assert.Equal(1, _sharedValue);
}
[Fact]
public void Test2()
{
Assert.Equal(1, _sharedValue); // 可能失败
}
}
// 好的做法 - 每个测试独立
public class GoodTests
{
[Fact]
public void Test1()
{
var value = 1;
Assert.Equal(1, value);
}
[Fact]
public void Test2()
{
var value = 1;
Assert.Equal(1, value);
}
}
4. 测试异常
验证异常抛出:
[Fact]
public void Divide_ByZero_ThrowsException()
{
// Arrange
var calc = new Calculator();
// Act & Assert
Assert.Throws<DivideByZeroException>(() => calc.Divide(1, 0));
// 或者更精确的断言
var ex = Assert.Throws<DivideByZeroException>(() => calc.Divide(1, 0));
Assert.Equal("Attempted to divide by zero.", ex.Message);
}
五、高级测试技术
1. 模糊测试(Fuzz Testing)
随机输入测试:
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("valid")]
public void ProcessInput_VariousInputs(string input)
{
var processor = new InputProcessor();
// 根据输入类型验证不同行为
if (string.IsNullOrWhiteSpace(input))
{
Assert.Throws<ArgumentException>(() => processor.Process(input));
}
else
{
var result = processor.Process(input);
Assert.NotNull(result);
}
}
2. 属性测试(Property-Based Testing)
使用FsCheck(需NuGet安装):
using FsCheck;
using FsCheck.Xunit;
[Property]
public bool Add_IsCommutative(int a, int b)
{
return new Calculator().Add(a, b) == new Calculator().Add(b, a);
}
[Property]
public bool Add_IsAssociative(int a, int b, int c)
{
return new Calculator().Add(new Calculator().Add(a, b), c)
== new Calculator().Add(a, new Calculator().Add(b, c));
}
3. 并发测试
验证线程安全:
[Fact]
public void ConcurrentAccess_ThreadSafe()
{
// Arrange
var collection = new ConcurrentDictionary<int, string>();
int threadCount = 10;
int iterations = 1000;
// Act
Parallel.For(0, threadCount, i =>
{
for (int j = 0; j < iterations; j++)
{
collection.TryAdd(j, $"Thread{i}");
}
});
// Assert
Assert.Equal(iterations, collection.Count);
}
六、持续集成中的测试
1. 测试自动化流程
典型CI流程:
- 代码提交 → 触发构建
- 编译 → 运行单元测试
- 测试通过 → 部署到测试环境
- 运行集成/性能测试
- 结果报告
GitHub Actions示例:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- name: Build
run: dotnet build --configuration Release
- name: Unit Tests
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Code Coverage
run: dotnet test --configuration Release /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
2. 测试结果分析
SonarQube集成:
- 安装SonarQube服务器
- 配置.NET项目分析
- 查看代码质量报告
示例SonarQube配置:
- name: SonarQube Analysis
uses: sonarsource/sonarcloud-github-action@master
with:
args: >
-Dsonar.projectKey=my_project
-Dsonar.organization=my_org
-Dsonar.host.url=https://sonarcloud.io
-Dsonar.login=$SONAR_TOKEN
-Dsonar.cs.opencover.reportsPaths=coverage.opencover.xml
七、测试性能优化
1. 测试并行执行
xUnit并行设置:
<!-- 在xunit.runner.json中 -->
{
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": 4
}
2. 测试选择性执行
按标记运行测试:
[Fact]
[Trait("Category", "Fast")]
public void FastTest() { /* ... */ }
[Fact]
[Trait("Category", "Slow")]
public void SlowTest() { /* ... */ }
// 命令行运行特定类别
dotnet test --filter Category=Fast
3. 测试数据隔离
使用内存数据库:
// 使用SQLite内存数据库进行测试
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new AppDbContext(options))
{
// 测试代码
}
八、常见测试问题解决
1. 测试不稳定(Flaky Tests)
解决方案:
- 添加等待条件(非固定等待)
- 隔离测试环境
- 添加重试机制
示例重试机制:
public static class RetryHelper
{
public static T ExecuteWithRetry<T>(Func<T> action, int retryCount = 3)
{
var exceptions = new List<Exception>();
for (int i = 0; i < retryCount; i++)
{
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
Thread.Sleep(500 * (i + 1));
}
}
throw new AggregateException(exceptions);
}
}
// 使用
var result = RetryHelper.ExecuteWithRetry(() =>
{
// 可能不稳定的操作
});
2. 测试速度慢
优化策略:
- 减少不必要的测试数据
- 使用轻量级模拟对象
- 并行执行测试
- 缓存昂贵资源
示例优化:
// 原始慢测试
[Fact]
public void SlowTest()
{
var data = LoadLargeDatasetFromDisk(); // 耗时操作
// ...
}
// 优化后
private static readonly List<Data> CachedData = LoadOnce();
private static List<Data> LoadOnce()
{
// 使用Lazy或静态构造函数确保只加载一次
return LoadLargeDatasetFromDisk();
}
[Fact]
public void OptimizedTest()
{
var data = CachedData; // 快速访问
// ...
}
九、测试工具推荐
1. 单元测试框架
- xUnit:现代、轻量、并行支持好
- NUnit:功能丰富、社区支持好
- MSTest:微软官方框架,与VS集成好
2. Mock框架
- Moq:最流行的Mock框架
- NSubstitute:语法简洁
- FakeItEasy:简单易用
3. 性能测试工具
- Apache JMeter:功能全面
- k6:开发者友好
- Gatling:高性能
4. 代码覆盖率
- Coverlet:与xUnit/NUnit集成
- OpenCover:功能全面
- dotCover:JetBrains出品
十、测试策略制定
1. 测试金字塔调整
┌─────────────┐
│ 手动探索测试 │
└─────────────┘
▲
│
▼
┌───────────────────┐
│ 端到端测试 │
└───────────────────┘
▲
│
▼
┌───────────────────────┐
│ 集成测试 │
└───────────────────────┘
▲
│
▼
┌─────────────────────────┐
│ 单元测试 │
└─────────────────────────┘
调整建议:
- 基础设施代码:增加集成测试比例
- UI代码:适当增加端到端测试
- 核心业务逻辑:保持高单元测试覆盖率
2. 测试环境配置
多环境测试策略:
- 开发环境:快速反馈的单元测试
- 集成环境:API契约测试
- 预发布环境:端到端测试
- 生产环境:监控+金丝雀测试
3. 测试数据管理
策略选择:
- 生成式:运行时生成(适合简单场景)
- 复制式:从生产环境脱敏(适合复杂场景)
- 混合式:关键数据真实+辅助数据生成
工具推荐:
- Bogus:C#数据生成库
- Faker.Net:类似Faker.js的实现
- SQL Data Generator:Redgate工具(商业)