在C#的单元测试开发过程中,测试替身Test Double是用来替代真实依赖对象的辅助对象,帮助开发者隔离被测试代码的外部依赖,让单元测试更加稳定高效。其中Stub、Mock、Fake、Spy是最常用的四种测试替身类型,它们的设计目标和适用场景存在明显差异。

测试替身Test Double的基础概念
测试替身Test Double是《xUnit测试模式》一书中提出的概念,泛指所有用来替代真实依赖的测试辅助对象。它的核心作用是隔离被测试对象和其他外部依赖,让单元测试可以专注于验证被测试对象自身的逻辑,不受外部依赖状态、网络、数据库等因素的干扰。
四种测试替身类型的定义与区别
1. Stub(桩)
Stub的核心作用是提供预设的返回值,用来替代真实依赖的返回结果,它不会验证调用行为,只关注返回内容是否符合预期。通常用于被测试对象需要调用外部依赖获取数据时,用Stub返回固定的测试数据,避免真实调用。
下面是一个C#中使用Stub的示例,假设我们有一个用户服务需要调用用户仓储获取用户信息:
// 用户实体类
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// 用户仓储接口
public interface IUserRepository
{
User GetById(int id);
}
// 用户服务类,被测试对象
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public string GetUserName(int id)
{
var user = _userRepository.GetById(id);
return user?.Name;
}
}
// Stub实现,替代真实的用户仓储
public class UserRepositoryStub : IUserRepository
{
public User GetById(int id)
{
// 返回预设的固定测试数据
return new User { Id = id, Name = "测试用户" };
}
}
// 单元测试示例
public class UserServiceTests
{
public void GetUserName_ValidId_ReturnsExpectedName()
{
// 创建Stub实例
IUserRepository stub = new UserRepositoryStub();
var service = new UserService(stub);
// 调用被测试方法
var result = service.GetUserName(1);
// 验证结果
Assert.Equal("测试用户", result);
}
}
2. Mock(模拟对象)
Mock和Stub的最大区别在于,Mock不仅会模拟依赖的返回值,还会验证依赖的调用行为,比如是否被正确调用、调用次数、传入参数是否符合预期。它更关注被测试对象和依赖之间的交互是否符合设计。
通常我们不会手动编写Mock类,而是使用Moq等Mock框架来生成,下面是使用Moq的示例:
// 需要引入Moq命名空间
using Moq;
public class UserServiceTests
{
public void GetUserName_ValidId_VerifyRepositoryCalled()
{
// 创建Mock对象
var mockRepo = new Mock<IUserRepository>();
// 设置Mock的返回值
mockRepo.Setup(repo => repo.GetById(1)).Returns(new User { Id = 1, Name = "测试用户" });
var service = new UserService(mockRepo.Object);
var result = service.GetUserName(1);
// 验证结果
Assert.Equal("测试用户", result);
// 验证GetById方法被调用了一次,且传入的参数是1
mockRepo.Verify(repo => repo.GetById(1), Times.Once());
}
}
3. Fake(伪对象)
Fake是有实际可工作的实现,但是它的实现不适合生产环境,通常用于替代那些依赖外部资源(如数据库、第三方接口)的真实对象。比如我们可以用内存集合来模拟数据库操作,实现一个Fake的仓储,避免真实连接数据库。
下面是Fake仓储的实现示例:
// Fake用户仓储实现
public class UserRepositoryFake : IUserRepository
{
// 使用内存集合存储数据,模拟数据库
private readonly List<User> _users = new List<User>
{
new User { Id = 1, Name = "用户A" },
new User { Id = 2, Name = "用户B" }
};
public User GetById(int id)
{
return _users.FirstOrDefault(u => u.Id == id);
}
// 额外添加保存方法,模拟数据库写入
public void Add(User user)
{
_users.Add(user);
}
}
// 单元测试示例
public class UserServiceTests
{
public void GetUserName_ExistingId_ReturnsCorrectName()
{
var fakeRepo = new UserRepositoryFake();
var service = new UserService(fakeRepo);
var result = service.GetUserName(1);
Assert.Equal("用户A", result);
}
}
4. Spy(间谍)
Spy的核心作用是记录依赖对象的调用信息,比如调用次数、传入的参数、调用顺序等,之后可以根据这些记录进行断言。它本质上是真实对象或者Stub的包装,增加了记录调用行为的能力,不会像Mock那样主动验证,而是把验证交给测试代码。
下面是手动实现Spy的示例:
// Spy实现,包装IUserRepository,记录调用信息
public class UserRepositorySpy : IUserRepository
{
private readonly IUserRepository _innerRepository;
// 记录GetById方法的调用参数
public List<int> GetByIdCallArgs { get; } = new List<int>();
// 记录GetById方法的调用次数
public int GetByIdCallCount => GetByIdCallArgs.Count;
public UserRepositorySpy(IUserRepository innerRepository)
{
_innerRepository = innerRepository;
}
public User GetById(int id)
{
// 记录调用参数
GetByIdCallArgs.Add(id);
// 调用内部真实仓储或者Stub的方法
return _innerRepository.GetById(id);
}
}
// 单元测试示例
public class UserServiceTests
{
public void GetUserName_CallSpy_VerifyCallInfo()
{
var stub = new UserRepositoryStub();
var spy = new UserRepositorySpy(stub);
var service = new UserService(spy);
service.GetUserName(1);
service.GetUserName(2);
// 验证调用次数
Assert.Equal(2, spy.GetByIdCallCount);
// 验证调用参数
Assert.Contains(1, spy.GetByIdCallArgs);
Assert.Contains(2, spy.GetByIdCallArgs);
}
}
四种类型的对比总结
为了更清晰地区分四种测试替身,我们可以通过下表对比它们的核心差异:
| 类型 | 核心作用 | 是否返回预设值 | 是否验证调用行为 | 是否有真实实现 |
|---|---|---|---|---|
| Stub | 提供预设返回值 | 是 | 否 | 否 |
| Mock | 验证调用行为+返回预设值 | 是 | 是 | 否 |
| Fake | 替代真实依赖的简化实现 | 视实现而定 | 否 | 是 |
| Spy | 记录调用信息供后续验证 | 视实现而定 | 否(仅记录) | 视实现而定 |
使用场景建议
- 当只需要依赖返回固定数据,不需要验证调用行为时,优先选择
Stub - 当需要验证被测试对象是否正确调用了依赖,比如调用次数、参数是否符合预期时,优先选择
Mock - 当真实依赖依赖外部资源(如数据库、文件、网络),需要简化实现避免外部依赖干扰时,优先选择
Fake - 当需要记录依赖的调用信息,但是验证逻辑比较复杂,不适合用Mock直接验证时,可以选择
Spy
Test_DoubleStubMockFakeSpy修改时间:2026-06-17 09:30:25