GFramework/docs/zh-CN/tutorials/unit-testing.md
GeWuYou fb14d7122c docs(style): 更新文档中的命名空间导入格式
- 将所有小写的命名空间导入更正为首字母大写格式
- 统一 GFramework 框架的命名空间引用规范
- 修复 core、ecs、godot 等模块的命名空间导入错误
- 标准化文档示例代码中的 using 语句格式
- 确保所有文档中的命名空间引用保持一致性
- 更新 global using 语句以匹配正确的命名空间格式
2026-03-10 07:18:49 +08:00

1148 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 单元测试实践
description: 学习如何为 GFramework 项目编写单元测试
---
# 单元测试实践
## 学习目标
完成本教程后,你将能够:
- 创建和配置测试项目
- 为架构组件Model、System、Controller编写单元测试
- 测试事件系统的发送和订阅功能
- 测试命令和查询的执行
- 使用 Moq 模拟依赖项
- 编写集成测试验证完整流程
- 理解测试最佳实践
## 前置条件
- 已安装 .NET SDK 8.0 或更高版本
- 了解 C# 基础语法
- 熟悉 xUnit 或 NUnit 测试框架
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[架构系统](/zh-CN/core/architecture)
## 步骤 1创建测试项目
首先,创建一个测试项目并添加必要的依赖。
### 1.1 创建测试项目
```bash
# 创建测试项目
dotnet new nunit -n MyGame.Tests
# 添加项目引用
cd MyGame.Tests
dotnet add reference ../MyGame/MyGame.csproj
```
### 1.2 配置项目文件
编辑 `MyGame.Tests.csproj`,添加必要的包引用:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- 测试框架 -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="NUnit" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
<!-- Mock 框架 -->
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>
<ItemGroup>
<!-- 项目引用 -->
<ProjectReference Include="..\MyGame\MyGame.csproj" />
</ItemGroup>
</Project>
```
### 1.3 创建 GlobalUsings.cs
创建 `GlobalUsings.cs` 文件,添加常用命名空间:
```csharp
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using NUnit.Framework;
global using Moq;
global using GFramework.Core.Abstractions.Architecture;
global using GFramework.Core.Abstractions.Model;
global using GFramework.Core.Abstractions.System;
```
## 步骤 2测试架构组件
### 2.1 测试 Model
Model 是数据层组件,我们需要测试其初始化和数据访问功能。
```csharp
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Model;
namespace MyGame.Tests.model;
/// <summary>
/// 测试模型类
/// </summary>
public sealed class TestModel : AbstractModel, ITestModel
{
public const int DefaultXp = 5;
public bool Initialized { get; private set; }
public int GetCurrentXp { get; } = DefaultXp;
public void Initialize()
{
Initialized = true;
}
public override void OnArchitecturePhase(ArchitecturePhase phase)
{
}
protected override void OnInit()
{
}
}
/// <summary>
/// Model 测试类
/// </summary>
[TestFixture]
public class TestModelTests
{
private TestModel _model = null!;
[SetUp]
public void SetUp()
{
_model = new TestModel();
}
[Test]
public void Model_Should_Have_Default_Xp()
{
// Assert
Assert.That(_model.GetCurrentXp, Is.EqualTo(TestModel.DefaultXp));
}
[Test]
public void Model_Should_Initialize_Correctly()
{
// Act
_model.Initialize();
// Assert
Assert.That(_model.Initialized, Is.True);
}
[Test]
public void Model_Should_Not_Be_Initialized_By_Default()
{
// Assert
Assert.That(_model.Initialized, Is.False);
}
}
```
### 2.2 测试 System
System 是业务逻辑层组件,测试其初始化和销毁功能。
```csharp
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.System;
namespace MyGame.Tests.system;
/// <summary>
/// 测试系统类
/// </summary>
public sealed class TestSystem : ISystem
{
private IArchitectureContext _context = null!;
public bool Initialized { get; private set; }
public bool DestroyCalled { get; private set; }
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
public void Initialize()
{
Initialized = true;
}
public void Destroy()
{
DestroyCalled = true;
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
}
/// <summary>
/// System 测试类
/// </summary>
[TestFixture]
public class TestSystemTests
{
private TestSystem _system = null!;
private Mock&lt;IArchitectureContext&gt; _mockContext = null!;
[SetUp]
public void SetUp()
{
_system = new TestSystem();
_mockContext = new Mock&lt;IArchitectureContext&gt;();
_system.SetContext(_mockContext.Object);
}
[Test]
public void System_Should_Initialize_Correctly()
{
// Act
_system.Initialize();
// Assert
Assert.That(_system.Initialized, Is.True);
}
[Test]
public void System_Should_Destroy_Correctly()
{
// Act
_system.Destroy();
// Assert
Assert.That(_system.DestroyCalled, Is.True);
}
[Test]
public void System_Should_Store_Context()
{
// Act
var context = _system.GetContext();
// Assert
Assert.That(context, Is.EqualTo(_mockContext.Object));
}
}
```
## 步骤 3测试事件系统
事件系统是框架的核心功能之一,需要测试事件的注册、发送和取消注册。
```csharp
using GFramework.Core.Events;
namespace MyGame.Tests.events;
/// <summary>
/// 测试事件类
/// </summary>
public class TestEvent
{
public int ReceivedValue { get; init; }
}
/// <summary>
/// 事件总线测试类
/// </summary>
[TestFixture]
public class EventBusTests
{
private EventBus _eventBus = null!;
[SetUp]
public void SetUp()
{
_eventBus = new EventBus();
}
[Test]
public void Register_Should_Add_Handler()
{
// Arrange
var called = false;
// Act
_eventBus.Register&lt;TestEvent&gt;(@event =&gt; { called = true; });
_eventBus.Send&lt;TestEvent&gt;();
// Assert
Assert.That(called, Is.True);
}
[Test]
public void UnRegister_Should_Remove_Handler()
{
// Arrange
var count = 0;
Action&lt;TestEvent&gt; handler = @event =&gt; { count++; };
// Act
_eventBus.Register(handler);
_eventBus.Send&lt;TestEvent&gt;();
Assert.That(count, Is.EqualTo(1));
_eventBus.UnRegister(handler);
_eventBus.Send&lt;TestEvent&gt;();
// Assert
Assert.That(count, Is.EqualTo(1));
}
[Test]
public void SendEvent_Should_Invoke_All_Handlers()
{
// Arrange
var count1 = 0;
var count2 = 0;
// Act
_eventBus.Register&lt;TestEvent&gt;(@event =&gt; { count1++; });
_eventBus.Register&lt;TestEvent&gt;(@event =&gt; { count2++; });
_eventBus.Send&lt;TestEvent&gt;();
// Assert
Assert.That(count1, Is.EqualTo(1));
Assert.That(count2, Is.EqualTo(1));
}
[Test]
public void SendEvent_Should_Pass_Event_Data()
{
// Arrange
var receivedValue = 0;
const int expectedValue = 100;
// Act
_eventBus.Register&lt;TestEvent&gt;(e =&gt; { receivedValue = e.ReceivedValue; });
_eventBus.Send(new TestEvent { ReceivedValue = expectedValue });
// Assert
Assert.That(receivedValue, Is.EqualTo(expectedValue));
}
}
```
## 步骤 4测试命令和查询
命令和查询是 CQRS 模式的核心,需要测试其执行逻辑。
### 4.1 测试 Command
```csharp
using GFramework.Core.Abstractions.CQRS.Command;
using GFramework.Core.Command;
namespace MyGame.Tests.command;
/// <summary>
/// 测试命令输入
/// </summary>
public sealed class TestCommandInput : ICommandInput
{
public int Value { get; init; }
}
/// <summary>
/// 测试命令
/// </summary>
public sealed class TestCommand : AbstractCommand&lt;TestCommandInput&gt;
{
public TestCommand(TestCommandInput input) : base(input)
{
}
public bool Executed { get; private set; }
public int ExecutedValue { get; private set; }
protected override void OnExecute(TestCommandInput input)
{
Executed = true;
ExecutedValue = input.Value;
}
}
/// <summary>
/// 带返回值的测试命令
/// </summary>
public sealed class TestCommandWithResult : AbstractCommand&lt;TestCommandInput, int&gt;
{
public TestCommandWithResult(TestCommandInput input) : base(input)
{
}
public bool Executed { get; private set; }
protected override int OnExecute(TestCommandInput input)
{
Executed = true;
return input.Value * 2;
}
}
/// <summary>
/// 命令执行器测试类
/// </summary>
[TestFixture]
public class CommandExecutorTests
{
private CommandExecutor _commandExecutor = null!;
[SetUp]
public void SetUp()
{
_commandExecutor = new CommandExecutor();
}
[Test]
public void Send_Should_Execute_Command()
{
// Arrange
var input = new TestCommandInput { Value = 42 };
var command = new TestCommand(input);
// Act
_commandExecutor.Send(command);
// Assert
Assert.That(command.Executed, Is.True);
Assert.That(command.ExecutedValue, Is.EqualTo(42));
}
[Test]
public void Send_WithNullCommand_Should_ThrowArgumentNullException()
{
// Act & Assert
Assert.Throws&lt;ArgumentNullException&gt;(() =&gt; _commandExecutor.Send(null!));
}
[Test]
public void Send_WithResult_Should_Return_Value()
{
// Arrange
var input = new TestCommandInput { Value = 100 };
var command = new TestCommandWithResult(input);
// Act
var result = _commandExecutor.Send(command);
// Assert
Assert.That(command.Executed, Is.True);
Assert.That(result, Is.EqualTo(200));
}
}
```
### 4.2 测试 Query
```csharp
using GFramework.Core.Abstractions.CQRS.Query;
using GFramework.Core.Query;
namespace MyGame.Tests.query;
/// <summary>
/// 测试查询输入
/// </summary>
public sealed class TestQueryInput : IQueryInput
{
public int Value { get; init; }
}
/// <summary>
/// 整数查询
/// </summary>
public sealed class TestQuery : AbstractQuery&lt;TestQueryInput, int&gt;
{
public TestQuery(TestQueryInput input) : base(input)
{
}
protected override int OnDo(TestQueryInput input)
{
return input.Value * 2;
}
}
/// <summary>
/// 字符串查询
/// </summary>
public sealed class TestStringQuery : AbstractQuery&lt;TestQueryInput, string&gt;
{
public TestStringQuery(TestQueryInput input) : base(input)
{
}
protected override string OnDo(TestQueryInput input)
{
return $"Result: {input.Value * 2}";
}
}
/// <summary>
/// 查询执行器测试类
/// </summary>
[TestFixture]
public class QueryExecutorTests
{
private QueryExecutor _queryExecutor = null!;
[SetUp]
public void SetUp()
{
_queryExecutor = new QueryExecutor();
}
[Test]
public void Send_Should_Return_Query_Result()
{
// Arrange
var input = new TestQueryInput { Value = 10 };
var query = new TestQuery(input);
// Act
var result = _queryExecutor.Send(query);
// Assert
Assert.That(result, Is.EqualTo(20));
}
[Test]
public void Send_WithNullQuery_Should_ThrowArgumentNullException()
{
// Act & Assert
Assert.Throws&lt;ArgumentNullException&gt;(() =&gt; _queryExecutor.Send&lt;int&gt;(null!));
}
[Test]
public void Send_WithStringResult_Should_Return_String()
{
// Arrange
var input = new TestQueryInput { Value = 5 };
var query = new TestStringQuery(input);
// Act
var result = _queryExecutor.Send(query);
// Assert
Assert.That(result, Is.EqualTo("Result: 10"));
}
}
```
## 步骤 5使用 Mock 和 Stub
使用 Moq 框架模拟依赖项,实现单元测试的隔离。
### 5.1 Mock 接口依赖
```csharp
using GFramework.Core.Abstractions.Model;
using GFramework.Core.System;
namespace MyGame.Tests.mock;
/// <summary>
/// 玩家数据接口
/// </summary>
public interface IPlayerModel : IModel
{
int GetScore();
void AddScore(int points);
}
/// <summary>
/// 游戏系统(依赖 IPlayerModel
/// </summary>
public class GameSystem : AbstractSystem
{
private IPlayerModel _playerModel = null!;
protected override void OnInit()
{
_playerModel = this.GetModel&lt;IPlayerModel&gt;();
}
public int CalculateBonus()
{
var score = _playerModel.GetScore();
return score * 2;
}
}
/// <summary>
/// Mock 测试示例
/// </summary>
[TestFixture]
public class MockExampleTests
{
private Mock&lt;IPlayerModel&gt; _mockPlayerModel = null!;
private Mock&lt;IArchitectureContext&gt; _mockContext = null!;
private GameSystem _gameSystem = null!;
[SetUp]
public void SetUp()
{
// 创建 Mock 对象
_mockPlayerModel = new Mock&lt;IPlayerModel&gt;();
_mockContext = new Mock&lt;IArchitectureContext&gt;();
// 设置 Mock 行为
_mockContext
.Setup(ctx =&gt; ctx.GetModel&lt;IPlayerModel&gt;())
.Returns(_mockPlayerModel.Object);
// 创建被测试对象
_gameSystem = new GameSystem();
_gameSystem.SetContext(_mockContext.Object);
_gameSystem.Initialize();
}
[Test]
public void CalculateBonus_Should_Return_Double_Score()
{
// Arrange
_mockPlayerModel.Setup(m =&gt; m.GetScore()).Returns(100);
// Act
var bonus = _gameSystem.CalculateBonus();
// Assert
Assert.That(bonus, Is.EqualTo(200));
}
[Test]
public void CalculateBonus_Should_Call_GetScore()
{
// Arrange
_mockPlayerModel.Setup(m =&gt; m.GetScore()).Returns(50);
// Act
_gameSystem.CalculateBonus();
// Assert
_mockPlayerModel.Verify(m =&gt; m.GetScore(), Times.Once);
}
}
```
### 5.2 验证方法调用
```csharp
namespace MyGame.Tests.mock;
[TestFixture]
public class VerificationTests
{
[Test]
public void Should_Verify_Method_Called_With_Specific_Arguments()
{
// Arrange
var mock = new Mock&lt;IPlayerModel&gt;();
// Act
mock.Object.AddScore(10);
// Assert
mock.Verify(m =&gt; m.AddScore(10), Times.Once);
}
[Test]
public void Should_Verify_Method_Never_Called()
{
// Arrange
var mock = new Mock&lt;IPlayerModel&gt;();
// Assert
mock.Verify(m =&gt; m.AddScore(It.IsAny&lt;int&gt;()), Times.Never);
}
[Test]
public void Should_Verify_Property_Access()
{
// Arrange
var mock = new Mock&lt;IPlayerModel&gt;();
mock.Setup(m =&gt; m.GetScore()).Returns(100);
// Act
var score = mock.Object.GetScore();
// Assert
mock.VerifyGet(m =&gt; m.GetScore(), Times.Once);
}
}
```
## 步骤 6集成测试
集成测试验证多个组件协同工作的完整流程。
### 6.1 创建测试架构
```csharp
using GFramework.Core.Architecture;
using GFramework.Core.Abstractions.Enums;
namespace MyGame.Tests.integration;
/// <summary>
/// 测试架构基类
/// </summary>
public abstract class TestArchitectureBase : Architecture&lt;TestArchitectureBase&gt;
{
public bool InitCalled { get; private set; }
public ArchitecturePhase CurrentPhase { get; private set; }
public List&lt;ArchitecturePhase&gt; PhaseHistory { get; } = new();
protected override void OnInitialize()
{
InitCalled = true;
}
public override void OnArchitecturePhase(ArchitecturePhase phase)
{
CurrentPhase = phase;
PhaseHistory.Add(phase);
base.OnArchitecturePhase(phase);
}
}
/// <summary>
/// 同步测试架构
/// </summary>
public sealed class SyncTestArchitecture : TestArchitectureBase
{
protected override void OnInitialize()
{
RegisterModel(new TestModel());
RegisterSystem(new TestSystem());
base.OnInitialize();
}
}
/// <summary>
/// 架构集成测试
/// </summary>
[TestFixture]
[NonParallelizable]
public class ArchitectureIntegrationTests
{
private SyncTestArchitecture? _architecture;
[SetUp]
public void SetUp()
{
_architecture = new SyncTestArchitecture();
}
[TearDown]
public async Task TearDown()
{
if (_architecture != null)
{
await _architecture.DestroyAsync();
_architecture = null;
}
}
[Test]
public void Architecture_Should_Initialize_All_Components_Correctly()
{
// Act
_architecture!.Initialize();
// Assert
Assert.That(_architecture.InitCalled, Is.True);
Assert.That(_architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Ready));
var context = _architecture.Context;
var model = context.GetModel&lt;TestModel&gt;();
Assert.That(model, Is.Not.Null);
Assert.That(model!.Initialized, Is.True);
var system = context.GetSystem&lt;TestSystem&gt;();
Assert.That(system, Is.Not.Null);
Assert.That(system!.Initialized, Is.True);
}
[Test]
public void Architecture_Should_Enter_Phases_In_Correct_Order()
{
// Act
_architecture!.Initialize();
// Assert
var phases = _architecture.PhaseHistory;
CollectionAssert.AreEqual(
new[]
{
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
ArchitecturePhase.Ready
},
phases
);
}
[Test]
public async Task Architecture_Destroy_Should_Destroy_All_Systems()
{
// Arrange
_architecture!.Initialize();
// Act
await _architecture.DestroyAsync();
// Assert
var system = _architecture.this.GetSystem&lt;TestSystem&gt;();
Assert.That(system!.DestroyCalled, Is.True);
Assert.That(_architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Destroyed));
}
[Test]
public void Event_Should_Be_Received()
{
// Arrange
_architecture!.Initialize();
var context = _architecture.Context;
var receivedValue = 0;
const int targetValue = 100;
// Act
context.RegisterEvent&lt;TestEvent&gt;(e =&gt; { receivedValue = e.ReceivedValue; });
context.SendEvent(new TestEvent { ReceivedValue = targetValue });
// Assert
Assert.That(receivedValue, Is.EqualTo(targetValue));
}
}
```
### 6.2 测试 BindableProperty
```csharp
using GFramework.Core.Property;
namespace MyGame.Tests.property;
[TestFixture]
public class BindablePropertyTests
{
[Test]
public void Value_Get_Should_Return_Default_Value()
{
// Arrange
var property = new BindableProperty&lt;int&gt;(5);
// Assert
Assert.That(property.Value, Is.EqualTo(5));
}
[Test]
public void Value_Set_Should_Trigger_Event()
{
// Arrange
var property = new BindableProperty&lt;int&gt;();
var receivedValue = 0;
// Act
property.Register(value =&gt; { receivedValue = value; });
property.Value = 42;
// Assert
Assert.That(receivedValue, Is.EqualTo(42));
}
[Test]
public void Value_Set_To_Same_Value_Should_Not_Trigger_Event()
{
// Arrange
var property = new BindableProperty&lt;int&gt;(5);
var count = 0;
// Act
property.Register(_ =&gt; { count++; });
property.Value = 5;
// Assert
Assert.That(count, Is.EqualTo(0));
}
[Test]
public void UnRegister_Should_Remove_Handler()
{
// Arrange
var property = new BindableProperty&lt;int&gt;();
var count = 0;
Action&lt;int&gt; handler = _ =&gt; { count++; };
// Act
property.Register(handler);
property.Value = 1;
Assert.That(count, Is.EqualTo(1));
property.UnRegister(handler);
property.Value = 2;
// Assert
Assert.That(count, Is.EqualTo(1));
}
[Test]
public void RegisterWithInitValue_Should_Call_Handler_Immediately()
{
// Arrange
var property = new BindableProperty&lt;int&gt;(5);
var receivedValue = 0;
// Act
property.RegisterWithInitValue(value =&gt; { receivedValue = value; });
// Assert
Assert.That(receivedValue, Is.EqualTo(5));
}
}
```
## 完整代码示例
以下是一个完整的测试项目结构示例:
```
MyGame.Tests/
├── GlobalUsings.cs
├── MyGame.Tests.csproj
├── model/
│ └── TestModelTests.cs
├── system/
│ └── TestSystemTests.cs
├── events/
│ └── EventBusTests.cs
├── command/
│ └── CommandExecutorTests.cs
├── query/
│ └── QueryExecutorTests.cs
├── mock/
│ ├── MockExampleTests.cs
│ └── VerificationTests.cs
├── property/
│ └── BindablePropertyTests.cs
└── integration/
└── ArchitectureIntegrationTests.cs
```
## 运行测试
### 使用命令行
```bash
# 运行所有测试
dotnet test
# 运行特定测试类
dotnet test --filter "FullyQualifiedName~EventBusTests"
# 运行特定测试方法
dotnet test --filter "FullyQualifiedName~EventBusTests.Register_Should_Add_Handler"
# 生成测试覆盖率报告
dotnet test --collect:"XPlat Code Coverage"
```
### 使用 IDE
在 Visual Studio 或 Rider 中:
1. 打开测试资源管理器
2. 选择要运行的测试
3. 点击"运行"或"调试"按钮
## 测试输出示例
```
正在启动测试执行,请稍候...
总共 1 个测试文件与指定模式匹配。
通过! - 失败: 0, 通过: 15, 跳过: 0, 总计: 15, 持续时间: 234 ms
测试运行成功。
测试总数: 15
通过: 15
总时间: 1.2345 秒
```
## 测试最佳实践
### 1. 遵循 AAA 模式
```csharp
[Test]
public void Example_Test()
{
// Arrange - 准备测试数据和依赖
var input = new TestInput { Value = 10 };
// Act - 执行被测试的操作
var result = PerformOperation(input);
// Assert - 验证结果
Assert.That(result, Is.EqualTo(20));
}
```
### 2. 测试命名规范
使用清晰的命名约定:
```csharp
// 格式: MethodName_Scenario_ExpectedBehavior
[Test]
public void Send_WithNullCommand_Should_ThrowArgumentNullException()
{
// ...
}
```
### 3. 一个测试一个断言
```csharp
// 好的做法
[Test]
public void Model_Should_Initialize()
{
_model.Initialize();
Assert.That(_model.Initialized, Is.True);
}
// 避免
[Test]
public void Model_Should_Work()
{
_model.Initialize();
Assert.That(_model.Initialized, Is.True);
Assert.That(_model.GetXp(), Is.EqualTo(5));
Assert.That(_model.Name, Is.Not.Null);
}
```
### 4. 使用 SetUp 和 TearDown
```csharp
[TestFixture]
public class MyTests
{
private MyClass _instance = null!;
[SetUp]
public void SetUp()
{
// 每个测试前执行
_instance = new MyClass();
}
[TearDown]
public void TearDown()
{
// 每个测试后执行
_instance?.Dispose();
}
}
```
### 5. 测试边界条件
```csharp
[Test]
[TestCase(0)]
[TestCase(-1)]
[TestCase(int.MinValue)]
public void AddScore_WithInvalidValue_Should_ThrowException(int invalidScore)
{
Assert.Throws&lt;ArgumentException&gt;(() =&gt; _model.AddScore(invalidScore));
}
```
### 6. 使用测试数据生成器
```csharp
[Test]
[TestCase(1, 2)]
[TestCase(5, 10)]
[TestCase(100, 200)]
public void CalculateBonus_Should_Return_Double(int input, int expected)
{
var result = Calculator.CalculateBonus(input);
Assert.That(result, Is.EqualTo(expected));
}
```
## 下一步
完成本教程后,你可以:
1. **提高测试覆盖率**
- 使用代码覆盖率工具(如 Coverlet
- 目标:达到 80% 以上的代码覆盖率
2. **学习 TDD测试驱动开发**
- 先写测试,再写实现
- 红-绿-重构循环
3. **集成 CI/CD**
- 在 GitHub Actions 中自动运行测试
- 配置测试失败时阻止合并
4. **性能测试**
- 使用 BenchmarkDotNet 进行性能测试
- 测试关键路径的性能
5. **探索高级测试技术**
- 参数化测试
- 数据驱动测试
- 快照测试
## 相关资源
- [NUnit 官方文档](https://docs.nunit.org/)
- [Moq 快速入门](https://github.com/moq/moq4/wiki/Quickstart)
- [架构设计模式](/zh-CN/best-practices/architecture-patterns)
- [性能优化最佳实践](/zh-CN/best-practices/performance)