掌握 xUnit 单元测试中的 Mock 与 Stub 实战
引言
上一章节介绍了 TDD 的三大法则,今天我们讲一下在单元测试中模拟对象的使用。
Fake
Fake-Fake是一个通用术语,可用于描述stub或mock对象。 它是stub还是mock取决于使用它的上下文。 也就是说,Fake可以是stub或mock
Mock-Mock对象是系统中的fake对象,用于确定单元测试是否通过。Mock起初为Fake,直到对其断言。
Stub-Stub是系统中现有依赖项的可控制替代项。 通过使用Stub,可以在无需使用依赖项的情况下直接测试代码。
参考 单元测试最佳做法 让我们使用相同的术语
区别点:
- Stub:
- 用于提供可控制的替代行为,通常是在测试中模拟依赖项的简单行为。
- 主要用于提供固定的返回值或行为,以便测试代码的特定路径。
- 不涉及对方法调用的验证,只是提供一个虚拟的实现。
- Mock:
- 用于验证方法的调用和行为,以确保代码按预期工作。
- 主要用于确认特定方法是否被调用,以及被调用时的参数和次数。
- 可以设置期望的调用顺序、参数和返回值,并在测试结束时验证这些调用。
总结:
- Stub 更侧重于提供一个简单的替代品,帮助测试代码路径,而不涉及行为验证。
- Mock 则更侧重于验证代码的行为和调用,以确保代码按预期执行。
在某些情况下两者可能看起来相似,但在测试的目的和用途上还是存在一些区别。在编写单元测试时,根据测试场景和需求选择合适的
stub或mock对象可以帮助提高测试的准确性和可靠性。
创建实战项目
创建一个 WebApi 的 Controller 项目,和一个EFCore仓储类库作为我们后续章节的演示项目
dotNetParadise-Xunit
│
├── src
│ ├── Sample.Api
│ └── Sample.Repository
Sample.Repository 是一个简单 EFCore 的仓储模式实现,Sample.Api 对外提供 RestFul 的 Api 接口
Sample.Repository 实现
- 第一步
Sample.Repository类库安装Nuget包
PM> NuGet\Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 8.0.3
PM> Microsoft.EntityFrameworkCore.Relational -Version 8.0.3
- 创建实体
Staff
public class Staff
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int? Age { get; set; }
public List<string>? Addresses { get; set; }
public DateTimeOffset? Created { get; set; }
}
- 创建
SampleDbContext数据库上下文
public class SampleDbContext(DbContextOptions<SampleDbContext> options) : DbContext(options)
{
public DbSet<Staff> Staff { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
- 定义仓储接口和实现
public interface IStaffRepository
{
/// <summary>
/// 获取 Staff 实体的 DbSet
/// </summary>
DbSet<Staff> dbSet { get; }
/// <summary>
/// 添加新的 Staff 实体
/// </summary>
/// <param name="staff"></param>
Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 Id 删除 Staff 实体
/// </summary>
/// <param name="id"></param>
Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// 更新 Staff 实体
/// </summary>
/// <param name="staff"></param>
Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 Id 获取单个 Staff 实体
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// 获取所有 Staff 实体
/// </summary>
/// <returns></returns>
Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 批量更新 Staff 实体
/// </summary>
/// <param name="staffList"></param>
Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default);
}
- 仓储实现
public class StaffRepository : IStaffRepository
{
private readonly SampleDbContext _dbContext;
public DbSet<Staff> dbSet => _dbContext.Set<Staff>();
public StaffRepository(SampleDbContext dbContext)
{
dbContext.Database.EnsureCreated();
_dbContext = dbContext;
}
public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default)
{
await dbSet.AddAsync(staff, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
{
//await dbSet.AsQueryable().Where(_ => _.Id == id).ExecuteDeleteAsync(cancellationToken);
var staff = await GetStaffByIdAsync(id, cancellationToken);
if (staff is not null)
{
dbSet.Remove(staff);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default)
{
dbSet.Update(staff);
_dbContext.Entry(staff).State = EntityState.Modified;
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await dbSet.AsQueryable().Where(_ => _.Id == id).FirstOrDefaultAsync(cancellationToken);
}
public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default)
{
return await dbSet.ToListAsync(cancellationToken);
}
public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default)
{
await dbSet.AddRangeAsync(staffList, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
- 依赖注入
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddEFCoreInMemoryAndRepository(this IServiceCollection services)
{
services.AddScoped<IStaffRepository, StaffRepository>();
services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("sample").EnableSensitiveDataLogging(), ServiceLifetime.Scoped);
return services;
}
}
到目前为止 仓储层的简单实现已经完成了,接下来完成
WebApi层
Sample.Api
将 Sample.Api 添加项目引用Sample.Repository
program 依赖注入
builder.Services.AddEFCoreInMemoryAndRepository();
- 定义
Controller
[Route("api/[controller]")]
[ApiController]
public class StaffController(IStaffRepository staffRepository) : ControllerBase
{
private readonly IStaffRepository _staffRepository = staffRepository;
[HttpPost]
public async Task<IResult> AddStaff([FromBody] Staff staff, CancellationToken cancellationToken = default)
{
await _staffRepository.AddStaffAsync(staff, cancellationToken);
return TypedResults.NoContent();
}
[HttpDelete("{id}")]
public async Task<IResult> DeleteStaff(int id, CancellationToken cancellationToken = default)
{
await _staffRepository.DeleteStaffAsync(id);
return TypedResults.NoContent();
}
[HttpPut("{id}")]
public async Task<Results<BadRequest<string>, NoContent, NotFound>> UpdateStaff(int id, [FromBody] Staff staff, CancellationToken cancellationToken = default)
{
if (id != staff.Id)
{
return TypedResults.BadRequest("Staff ID mismatch");
}
var originStaff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
if (originStaff is null) return TypedResults.NotFound();
originStaff.Update(staff);
await _staffRepository.UpdateStaffAsync(originStaff, cancellationToken);
return TypedResults.NoContent();
}
[HttpGet("{id}")]
public async Task<Results<Ok<Staff>, NotFound>> GetStaffById(int id, CancellationToken cancellationToken = default)
{
var staff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
if (staff == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(staff);
}
[HttpGet]
public async Task<IResult> GetAllStaff(CancellationToken cancellationToken = default)
{
var staffList = await _staffRepository.GetAllStaffAsync(cancellationToken);
return TypedResults.Ok(staffList);
}
[HttpPost("BatchAdd")]
public async Task<IResult> BatchAddStaff([FromBody] List<Staff> staffList, CancellationToken cancellationToken = default)
{
await _staffRepository.BatchAddStaffAsync(staffList, cancellationToken);
return TypedResults.NoContent();
}
}
F5 项目跑一下

到这儿我们的项目已经创建完成了本系列后面的章节基本上都会以这个项目为基础展开拓展
控制器的单元测试
[单元测试涉及通过基础结构和依赖项单独测试应用的一部分。 单元测试控制器逻辑时,仅测试单个操作的内容,不测试其依赖项或框架自身的行为。
本章节主要以控制器的单元测试来带大家了解一下Stup和Moq的核心区别。
创建一个新的测试项目,然后添加Sample.Api的项目引用

Stub 实战
Stub 是系统中现有依赖项的可控制替代项。通过使用 Stub,可以在测试代码时不需要使用真实依赖项。通常情况下,存根最初被视为 Fake
下面对 StaffController 利用 Stub 进行单元测试,
- 创建一个
Stub实现IStaffRepository接口,以模拟对数据库或其他数据源的访问操作。 - 在单元测试中使用这个
Stub替代IStaffRepository的实际实现,以便在不依赖真实数据源的情况下测试StaffController中的方法。
我们在dotNetParadise.FakeTest测试项目上新建一个IStaffRepository的实现,名字可以叫StubStaffRepository
public class StubStaffRepository : IStaffRepository
{
public DbSet<Staff> dbSet => default!;
public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken)
{
// 模拟添加员工操作
await Task.CompletedTask;
}
public async Task DeleteStaffAsync(int id)
{
// 模拟删除员工操作
await Task.CompletedTask;
}
public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken)
{
// 模拟更新员工操作
await Task.CompletedTask;
}
public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken)
{
// 模拟根据 ID 获取员工操作
return await Task.FromResult(new Staff { Id = id, Name = "Mock Staff" });
}
public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken)
{
// 模拟获取所有员工操作
return await Task.FromResult(new List<Staff> { new Staff { Id = 1, Name = "Mock Staff 1" }, new Staff { Id = 2, Name = "Mock Staff 2" } });
}
public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken)
{
// 模拟批量添加员工操作
await Task.CompletedTask;
}
public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
}
}
我们新创建了一个仓储的实现来替换StaffRepository作为新的依赖
下一步在单元测试项目测试我们的Controller方法
public class TestStubStaffController
{
[Fact]
public async Task AddStaff_WhenCalled_ReturnNoContent()
{
//Arrange
var staffController = new StaffController(new StubStaffRepository());
var staff = new Staff()
{
Age = 10,
Name = "Test",
Email = "Test@163.com",
Created = DateTimeOffset.Now,
};
//Act
var result = await staffController.AddStaff(staff);
//Assert
Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
}
[Fact]
public async Task GetStaffById_WhenCalled_ReturnOK()
{
//Arrange
var staffController = new StaffController(new StubStaffRepository());
var id = 1;
//Act
var result = await staffController.GetStaffById(id);
//Assert
Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
var okResult = (Ok<Staff>)result.Result;
Assert.Equal(id, okResult.Value?.Id);
}
//先暂时省略后面测试方法....
}

用
Stub来替代真实的依赖项,以便更好地控制测试环境和测试结果
Mock
在测试过程中,尤其是
TDD的开发过程中,测试用例有限开发在这个时候,我们总是要去模拟对象的创建,这些对象可能是某个接口的实现也可能是具体的某个对象,这时候就必须去写接口的实现,这时候模拟对象Mock的用处就体现出来了,在社区中也有很多模拟对象的库如Moq,FakeItEasy等。
Moq是一个简单、直观且强大的.NET模拟库,用于在单元测试中模拟对象和行为。通过Moq,您可以轻松地设置依赖项的行为,并验证代码的调用。
我们用上面的实例来演示一下Moq的核心用法
第一步 Nuget 包安装Moq
PM> NuGet\Install-Package Moq -Version 4.20.70
您可以使用 Moq 中的 Setup 方法来设置模拟对象(Mock 对象)中可重写方法的行为,结合 Returns(用于返回一个值)或 Throws(用于抛出异常)等方法来定义其行为。这样可以模拟对特定方法的调用,使其在测试中返回预期的值或抛出特定的异常。
创建TestMockStaffController测试类,接下来我们用Moq实现一下上面的例子
public class TestMockStaffController
{
private readonly ITestOutputHelper _testOutputHelper;
public TestMockStaffController(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Fact]
public async Task AddStaff_WhenCalled_ReturnNoContent()
{
//Arrange
var mock = new Mock<IStaffRepository>();
mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
var staffController = new StaffController(mock.Object);
var staff = new Staff()
{
Age = 10,
Name = "Test",
Email = "Test@163.com",
Created = DateTimeOffset.Now,
};
//Act
var result = await staffController.AddStaff(staff);
//Assert
Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
}
[Fact]
public async Task GetStaffById_WhenCalled_ReturnOK()
{
//Arrange
var mock = new Mock<IStaffRepository>();
var id = 1;
mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).ReturnsAsync(() => new Staff()
{
Id = id,
Name = "张三",
Age = 18,
Email = "zhangsan@163.com",
Created = DateTimeOffset.Now
});
var staffController = new StaffController(mock.Object);
//Act
var result = await staffController.GetStaffById(id);
//Assert
Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
var okResult = (Ok<Staff>)result.Result;
Assert.Equal(id, okResult.Value?.Id);
_testOutputHelper.WriteLine(okResult.Value?.Name);
}
//先暂时省略后面测试方法....
}
看一下运行测试

Moq 核心功能讲解
通过我们上面这个简单的
Demo简单的了解了一下 Moq 的使用,接下来我们对Moq和核心功能深入了解一下
通过安装的Nuget包可以看到, Moq依赖了Castle.Core这个包,Moq正是利用了 Castle 来实现动态代理模拟对象的功能。
基本概念
Mock对象:通过Moq创建的模拟对象,用于模拟外部依赖项的行为。//创建Mock对象
var mock = new Mock<IStaffRepository>();
Setup:用于设置Mock对象的行为和返回值,以指定当调用特定方法时应该返回什么结果。//指定调用AddStaffAsync方法的参数行为
mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
异步方法
从我们上面的单元测试中看到我们使用了一个异步方法,使用返回值ReturnsAsync表示的
mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
.ReturnsAsync(() => new Staff()
{
Id = id,
Name = "张三",
Age = 18,
Email = "zhangsan@163.com",
Created = DateTimeOffset.Now
});
Moq有三种方式去设置异步方法的返回值分别是:
使用 .Result 属性(Moq 4.16 及以上版本):
- 在 Moq 4.16 及以上版本中,您可以直接通过
mock.Setup返回任务的.Result属性来设置异步方法的返回值。这种方法几乎适用于所有设置和验证表达式。 - 示例:
mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
- 在 Moq 4.16 及以上版本中,您可以直接通过
使用 ReturnsAsync(较早版本):
- 在较早版本的 Moq 中,您可以使用类似
ReturnsAsync、ThrowsAsync等辅助方法来设置异步方法的返回值。 - 示例:
mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);
- 在较早版本的 Moq 中,您可以使用类似
使用 Lambda 表达式:
- 您还可以使用 Lambda 表达式来返回异步方法的结果。不过这种方式会触发有关异步 Lambda 同步执行的编译警告。
- 示例:
mock.Setup(foo => foo.DoSomethingAsync()).Returns(async () => 42);
参数匹配
在我们单元测试实例中用到了参数匹配,mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).,对就是这个It.IsAny<int>(),此处的用意是匹配任意输入的 int类型的入参,接下来我们一起看下参数匹配的一些常用示例。
任意值匹配
It.IsAny<T>()mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
ref 参数的任意值匹配:
对于 ref 参数,可以使用 It.Ref.IsAny 进行匹配(需要 Moq 4.8 或更高版本)。//Arrange
var mock = new Mock<IFoo>();
// ref arguments
var instance = new Bar();
// Only matches if the ref argument to the invocation is the same instance
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);
匹配满足条件的值:
使用It.Is<T>(predicate)可以匹配满足条件的值,其中predicate是一个函数。//匹配满足条件的值
mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
//It.Is 断言
var result = mock.Object.Add(3);
Assert.False(result);
匹配范围:
使用It.IsInRange<T>可以匹配指定范围内的值mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Moq.Range.Inclusive))).Returns(true);
var inRangeResult = mock.Object.Add(3);
Assert.True(inRangeResult);
匹配正则表达式:
使用It.IsRegex可以匹配符合指定正则表达式的值{
mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo");
var result = mock.Object.DoSomethingStringy("a");
Assert.Equal("foo", result);
}
属性值
- 设置属性的返回值
通过Setup后的Returns函数 设置Mock的返回值{
mock.Setup(foo => foo.Name).Returns("bar");
Assert.Equal("bar",mock.Object.Name);
}
SetupSet设置属性的设置行为,期望特定值被设置.
主要是通过设置预期行为,对属性值做一些验证或者回调等操作//SetupUp
mock = new Mock<IFoo>();
// Arrange
mock.SetupSet(foo => foo.Name = "foo").Verifiable();
//Act
mock.Object.Name = "foo";
mock.Verify();
如果值设置为mock.Object.Name = "foo1";,
单元测试就会抛出异常
OutPut:
dotNetParadise.FakeTest.TestControllers.TestMockStaffController.Test_Moq_Demo
源: TestMockStaffController.cs 行 70
持续时间: 8.7 秒
消息:
Moq.MockException : Mock<IFoo:2>:
This mock failed verification due to the following:
IFoo foo => foo.Name = "foo":
This setup was not matched.
堆栈跟踪:
Mock.Verify(Func`2 predicate, HashSet`1 verifiedMocks) 行 309
Mock.Verify() 行 251
TestMockStaffController.Test_Moq_Demo() 行 111
--- End of stack trace from previous location ---
VerifySet直接验证属性的设置操作
//VerifySet直接验证属性的设置操作
{
// Arrange
mock = new Mock<IFoo>();
//Act
mock.Object.Name = "foo";
//Asset
mock.VerifySet(person => person.Name = "foo");
}
SetupProperty
使用SetupProperty可以为Mock对象的属性设置行为,包括get和set的行为。
{
// Arrange
mock = new Mock<IFoo>();
// start "tracking" sets/gets to this property
mock.SetupProperty(f => f.Name);
// alternatively, provide a default value for the stubbed property
mock.SetupProperty(f => f.Name, "foo");
//Now you can do:
IFoo foo = mock.Object;
// Initial value was stored
//Asset
Assert.Equal("foo", foo.Name);
}
在Moq 中,您可以使用 SetupAllProperties 方法来一次性存根(Stub)Mock 对象的所有属性。这意味着所有属性都会开始跟踪其值,并可以提供默认值。以下是一个示例演示如何使用 SetupAllProperties 方法:
// 存根(Stub)Mock 对象的所有属性
mock.SetupAllProperties();
通过使用 SetupProperty 方法,可以更灵活地设置 Mock 对象的属性行为和默认值,以满足单元测试中的需求
处理事件(Events)
在 Moq 4.13 及以后的版本中,你可以通过配置事件的 add 和 remove 访问器来模拟事件的行为。这允许你指定当事件处理器被添加或移除时应该发生的逻辑。这通常用于验证事件是否被正确添加或移除,或者模拟事件触发时的行为。
SetupAdd用于设置Mock对象的事件的add访问器,即用于模拟事件订阅的行为
SetupRemove用于设置Mock对象的事件的remove访问器,以模拟事件处理程序的移除行为
创建要被测试的类:
public class HasEvent
{
public virtual event Action Event;
public void RaiseEvent() => this.Event?.Invoke();
}
{
var handled = false;
var mock = new Mock<HasEvent>();
//设置订阅行为
mock.SetupAdd(m => m.Event += It.IsAny<Action>()).CallBase();
// 订阅事件并设置事件处理逻辑
Action eventHandler = () => handled = true;
mock.Object.Event += eventHandler;
mock.Object.RaiseEvent();
Assert.True(handled);
// 重置标志为 false
handled = false;
// 移除事件处理程序
mock.SetupRemove(h => h.Event -= It.IsAny<Action>()).CallBase();
// 移除事件处理程序
mock.Object.Event -= eventHandler;
// 再次触发事件
mock.Object.RaiseEvent();
// Assert - 验证事件是否被正确处理
Assert.False(handled); // 第一次应该为 true,第二次应该为 false
}
这段代码是一个针对 HasEvent 类的测试示例,使用 Moq 来设置事件的订阅和移除行为,并验证事件处理程序的添加和移除是否按预期工作。让我简单解释一下这段代码的流程:
- 创建一个 Mock 对象
mock,模拟HasEvent类。 - 使用
SetupAdd方法设置事件的订阅行为,并使用CallBase方法调用基类的实现。 - 订阅事件并设置事件处理逻辑,将事件处理程序
eventHandler添加到事件中。 - 调用
RaiseEvent方法触发事件,并通过断言验证事件处理程序是否被正确处理。 - 将
handled标志重置为false。 - 使用
SetupRemove方法设置事件的移除行为,并使用CallBase方法调用基类的实现。 - 移除事件处理程序
eventHandler。 - 再次触发事件,并通过断言验证事件处理程序是否被正确移除。
通过这个测试示例,可以验证事件处理程序的添加和移除操作是否正常工作
Raise
Raise方法用于手动触发 Mock 对象上的事件,模拟事件的触发过程
{
// Arrange
var handled = false;
var mock = new Mock<HasEvent>();
//设置订阅行为
mock.Object.Event += () => handled = true;
//act
mock.Raise(m => m.Event += null);
// Assert - 验证事件是否被正确处理
Assert.True(handled);
}
这个示例使用Raise方法手动触发 Mock 对象上的事件 Event,并验证事件处理程序的执行情况。通过设置事件的订阅行为,触发事件,以及断言验证事件处理程序的执行结果,测试了事件处理程序的逻辑是否按预期执行。这个过程帮助我们确认事件处理程序在事件触发时能够正确执行.
Callbacks
Callback方法用于在设置 Mock 对象的成员时指定回调操作。当特定操作被调用时,可以在 Callback 方法中执行自定义的逻辑
//Arrange
var mock = new Mock<IFoo>();
var calls = 0;
var callArgs = new List<string>();
mock.Setup(foo => foo.DoSomething("ping"))
.Callback(() => calls++)
.Returns(true);
// Act
mock.Object.DoSomething("ping");
// Assert
Assert.Equal(1, calls); // 验证 DoSomething 方法被调用一次
在调用 DoSomething 方法是,回调操作自动被触发参数++
CallBack捕获参数
//CallBack 捕获参数
{
//Arrange
mock = new Mock<IFoo>();
mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
.Callback<string>(s => callArgs.Add(s))
.Returns(true);
//Act
mock.Object.DoSomething("a");
//Asset
// 验证参数是否被添加到 callArgs 列表中
Assert.Contains("a", callArgs);
}
使用
Moq的Callback方法可以捕获方法调用时的参数,允许我们在测试中访问和处理这些参数。通过在Setup方法中指定Callback操作,我们可以捕获方法调用时传入的参数,并在回调中执行自定义逻辑,例如将参数添加到列表中。这种方法可以帮助我们验证方法在不同参数下的行为,以及检查方法是否被正确调用和传递参数。总的来说,Callback方法为我们提供了一种灵活的方式来处理方法调用时的参数,帮助我们编写更全面的单元测试。
SetupProperty
SetupProperty方法可用于设置Mock对象的属性,并为其提供getter和setter。
{
//Arrange
mock = new Mock<IFoo>();
mock.SetupProperty(foo => foo.Name);
mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
.Callback((string s) => mock.Object.Name = s)
.Returns(true);
//Act
mock.Object.DoSomething("a");
// Assert
Assert.Equal("a", mock.Object.Name);
}
SetupProperty 方法的作用包括:
设置属性的初始值:通过
SetupProperty方法,我们可以设置Mock对象属性的初始值,使其在测试中具有特定的初始状态。模拟属性的 getter 和 setter:
SetupProperty方法允许我们为属性设置getter和setter,使我们能够访问和修改属性的值。捕获属性的设置操作:在设置
Mock对象的属性时,可以使用Callback方法捕获设置操作,以执行自定义逻辑或记录属性的设置情况。验证属性的行为:通过设置属性和相应的行为,可以验证属性的行为是否符合预期,以确保代码的正确性和可靠性
Verification
在 Moq 中,Verification 是指验证 Mock 对象上的方法是否被正确调用,以及调用时是否传入了预期的参数。通过 Verification,我们可以确保 Mock 对象的方法按预期进行了调用,从而验证代码的行为是否符合预期。
{
//Arrange
var mock = new Mock<IFoo>();
//Act
mock.Object.Add(1);
// Assert
mock.Verify(foo => foo.Add(1));
}
- 验证方法被调用的行为
- 未被调用,或者调用至少一次
{
var mock = new Mock<IFoo>();
mock.Verify(foo => foo.DoSomething("ping"), Times.Never());
}
mock.Verify(foo => foo.DoSomething("ping"), Times.AtLeastOnce());
Verify指定 Times.AtLeastOnce() 验证方法至少被调用了一次。
- VerifySet
验证是否是按续期设置,上面有讲过。
- VerifyGet
用于验证属性的getter方法至少被访问指定次数,或者没有被访问.
{
var mock = new Mock<IFoo>();
mock.VerifyGet(foo => foo.Name);
}
- VerifyAdd,VerifyRemove
VerifyAdd 和 VerifyRemove 方法来验证事件的订阅和移除
// Verify event accessors (requires Moq 4.13 or later):
mock.VerifyAdd(foo => foo.FooEvent += It.IsAny<EventHandler>());
mock.VerifyRemove(foo => foo.FooEvent -= It.IsAny<EventHandler>());
- VerifyNoOtherCalls
VerifyNoOtherCalls 方法的作用是在使用 Moq 进行方法调用验证时,确保除了已经通过 Verify 方法验证过的方法调用外,没有其他未验证的方法被执行
mock.VerifyNoOtherCalls();
Customizing Mock Behavior
- MockBehavior.Strict
使用Strict模式创建的Mock对象时,如果发生了未设置期望的方法调用,包括未设置对方法的期望行为(如返回值、抛出异常等),则在该未设置期望的方法调用时会抛出MockException异常。这意味着在Strict模式下,Mock对象会严格要求所有的方法调用都必须有对应的期望设置,否则会触发异常。
[Fact]
public void TestStrictMockBehavior_WithUnsetExpectation()
{
// Arrange
var mock = new Mock<IFoo>(MockBehavior.Strict);
//mock.Setup(_ => _.Add(It.IsAny<int>())).Returns(true);
// Act & Assert
Assert.Throws<MockException>(() => mock.Object.Add(3));
}
如果mock.Setup这一行注释了,即未设置期望值,则会抛出异常
- CallBase
在上面的示例中我们也能看到CallBase的使用
在Moq中,通过设置CallBase = true,可以创建一个部分模拟对象(Partial Mock),这样在没有设置期望的成员时,会调用基类的实现。这在需要模拟部分行为并保留基类实现的场景中很有用,特别适用于模拟System.Web中的Web/Html控件。
public interface IUser
{
string GetName();
}
public class UserBase : IUser
{
public virtual string GetName()
{
return "BaseName";
}
string IUser.GetName() => "Name";
}
测试
[Fact]
public void TestPartialMockWithCallBase()
{
// Arrange
var mock = new Mock<UserBase> { CallBase = true };
mock.As<IUser>().Setup(foo => foo.GetName()).Returns("MockName");
// Act
string result = mock.Object.GetName();//
// Assert
Assert.Equal("BaseName", result);
//Act
var valueOfSetupMethod = ((IUser)mock.Object).GetName();
//Assert
Assert.Equal("MockName", valueOfSetupMethod);
}
- 第一个
Act:调用模拟对象的 GetName() 方法,此时基类的实现被调用,返回值为"BaseName"。 - 第二个
Act/通过强制类型转换将模拟对象转换为 IUser 接口类型,调用接口方法 GetName(),返回值为"MockName"。
- DefaultValue.Mock
创建一个自动递归模拟对象,该模拟对象在没有期望的成员上返回新的模拟对象
[Fact]
public void TestRecursiveMock()
{
// Arrange
var mock = new Mock<IFoo> { DefaultValue = DefaultValue.Mock };
// Act
Bar value = mock.Object.Bar;
var barMock = Mock.Get(value);
barMock.Setup(b => b.Submit()).Returns(true);
// Assert
Assert.True(mock.Object.Bar.Submit());
}
在这个示例中,IFoo 接口具有一个虚拟属性 Bar,Bar 类有一个虚拟方法 Submit。通过设置 DefaultValue.Mock,我们创建了一个自动递归模拟对象 mock,在访问 Bar 属性时会返回一个新的模拟对象。然后,我们对返回的 Bar 模拟对象设置了期望行为,并验证了其提交方法的返回值。这样,您可以方便地管理和设置递归模拟对象的期望行为。
**MockRepository**
通过使用MockRepository,可以更加方便地集中管理和验证所有模拟对象,同时确保它们的设置和验证是一致的[Fact]
public void TestRepositoryMock()
{
// Create a MockRepository with MockBehavior.Strict and DefaultValue.Mock
var repository = new MockRepository(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock }; // Create a mock using the repository settings
var fooMock = repository.Create<IFoo>(); // Create a mock overriding the repository settings with MockBehavior.Loose
var barMock = repository.Create<Bar>(MockBehavior.Loose); // Verify all verifiable expectations on all mocks created through the repository
repository.Verify(); // Additional setup and assertions can be done on fooMock and barMock as needed
// For example:
barMock.Setup(b => b.Submit()).Returns(true);
Assert.True(barMock.Object.Submit());
}
我们首先创建了一个
MockRepository,并设置了MockBehavior.Strict和DefaultValue.Mock。然后通过repository.Create<T>()方法在MockRepository设置下创建了一个IFoo接口的模拟对象fooMock,以及一个使用MockBehavior.Loose的Bar类的模拟对象barMock。最后,我们调用repository.Verify()来验证通过MockRepository创建的所有模拟对象上的所有可验证期望。
Miscellaneous
Reset
可以使用 Reset() 方法来重置模拟对象,清除所有的设置、默认返回值、注册的事件处理程序以及所有记录的调用。这在测试场景中特别有用,可以确保每个测试用例在独立的环境下运行,避免测试之间的相互影响
mock.Reset();
SetupSequence
可以使用 SetupSequence 方法来设置一个成员在连续调用时返回不同的值或抛出异常。这在需要模拟一个成员在多次调用时具有不同行为的场景中非常有用
[Fact]
public void TestSetupSequence()
{
// Arrange
var mock = new Mock<IFoo>();
mock.SetupSequence(f => f.GetCount())
.Returns(3)
.Returns(2)
.Returns(1)
.Returns(0)
.Throws(new InvalidOperationException());
// Act & Assert
Assert.Equal(3, mock.Object.GetCount());
Assert.Equal(2, mock.Object.GetCount());
Assert.Equal(1, mock.Object.GetCount());
Assert.Equal(0, mock.Object.GetCount());
Assert.Throws<InvalidOperationException>(() => mock.Object.GetCount());
}
LINQ to Mocks
LINQ to Mocks 是 Moq 提供的一种声明性规范查询方式,使得您可以通过 LINQ 风格的语法来指定模拟对象的行为。通过 LINQ to Mocks,您可以从模拟对象的宇宙中获取符合特定规范的模拟对象,从而更加直观地设置模拟对象的行为
var services = Mock.Of<IServiceProvider>(sp =>
sp.GetService(typeof(IRepository)) == Mock.Of<IRepository>(r => r.IsAuthenticated == true) &&
sp.GetService(typeof(IAuthentication)) == Mock.Of<IAuthentication>(a => a.AuthenticationType == "OAuth"));
// Multiple setups on a single mock and its recursive mocks
ControllerContext context = Mock.Of<ControllerContext>(ctx =>
ctx.HttpContext.User.Identity.Name == "kzu" &&
ctx.HttpContext.Request.IsAuthenticated == true &&
ctx.HttpContext.Request.Url == new Uri("http://moq.github.io/moq4/") &&
ctx.HttpContext.Response.ContentType == "application/xml");
// Setting up multiple chained mocks:
var context = Mock.Of<ControllerContext>(ctx =>
ctx.HttpContext.Request.Url == new Uri("http://moqthis.me") &&
ctx.HttpContext.Response.ContentType == "application/xml" &&
// Chained mock specification
ctx.HttpContext.GetSection("server") == Mock.Of<ServerSection>(config =>
config.Server.ServerUrl == new Uri("http://moqthis.com/api")));
最后
这篇总结详细介绍了在单元测试中模拟对象的使用,包括 Fake、Mock 和 Stub 的概念及区别。针对 Moq 的核心功能进行了深入讲解,包括参数匹配、事件处理、回调操作、属性值设置、验证方法调用等内容。此外,还介绍了一些高级功能如自定义模拟对象行为、重置模拟对象、设置序列返回值、以及 LINQ to Mocks 的使用方式,后续章节开始我们的单元测试实战啦。
掌握 xUnit 单元测试中的 Mock 与 Stub 实战的更多相关文章
- 单元测试:单元测试中的mock
公司要求提升单元测试的质量,提高代码的分支覆盖率和行覆盖率,安排我研究单元测试,指定方案分享并在开发部普及开.整理完资料后,同步一下到博客. 单元测试中的mock的目的 mock的主要目的是让单元测试 ...
- 单元测试中使用mock最好不要使用easymock而应该使用powermock
视频参考汪文君powermock视频教程相当的经典
- 利用Python中的mock库对Python代码进行模拟测试
这篇文章主要介绍了利用Python中的mock库对Python代码进行模拟测试,mock库自从Python3.3依赖成为了Python的内置库,本文也等于介绍了该库的用法,需要的朋友可以参考下 ...
- 【转】利用Python中的mock库对Python代码进行模拟测试
出处 https://www.toptal.com/python/an-introduction-to-mocking-in-python http://www.oschina.net/transla ...
- 单元测试中使用Moq对EF的DbSet进行mock
刚用上Moq,就用它解决了一个IUnitOfWork的mock问题,在这篇博文中记录一下. 开发场景 Application服务层BlogCategoryService的实现代码如下: public ...
- 单元测试Mockito中的Mock和Spy
转载:https://blog.csdn.net/qq_30141957/article/details/81273829 项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能 ...
- 单元测试系列:Mock工具之Mockito实战
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...
- 单元测试系列:Mock工具Jmockit使用介绍
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...
- 单元测试系列之二:Mock工具Jmockit实战
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...
- [转]软件测试- 3 - Mock 和Stub的区别
由于一直没有完全搞明白Mock和Stub的区别,所以查了很多文章,而这一篇是做好的: http://yuan.iteye.com/blog/470418 尤其是8楼,Frostred的发言,描述地相当 ...
随机推荐
- 【Azure Developer】使用Python代码获取VM的IP地址 (Public IP + Private IP)【未解决问题标签】
记录使用以下的代码获取Azure VM中的IP地址 """Create and manage virtual machines. This script expects ...
- 图数据库 Nebula Graph 的代码变更测试覆盖率实践
对于一个持续开发的大型工程而言,足够的测试是保证软件行为符合预期的有效手段,而不是仅仅依靠 code review 或者开发者自己的技术素质.测试的编写理想情况下应该完全定义软件的行为,但是通常情况都 ...
- 【架构师视角系列】QConfig配置中心系列之Server端(三)
声明 原创文章,转载请标注.https://www.cnblogs.com/boycelee/p/17993697 <码头工人的一千零一夜>是一位专注于技术干货分享的博主,追随博主的文章, ...
- 关于黑客网络 for linux,这个游戏的启动解决方法
原帖位置https://tieba.baidu.com/p/6200215090
- git 修改大小写远程分支失败
先说原因: windows大小写不敏感导致 最近由于项目需求,改回了windows开发环境,之前一直是 mac 开发环境,结果在windows环境下使用 git 时, 把某个之前大写的文件夹名称改 ...
- 算法研究之快速排序java版
很早之前就已经接触过快速排序算法了,面试当中也屡屡被问到,虽然明白其原理,但从未真正的用代码敲出来. 写关于算法的代码之前一定要原理想明白,不然就是盲目,在参考有关资料及自己的沉思之后,写出如下代码, ...
- leetcode数据库sql之Rising Temperature
leetcode原文引用: Given a Weather table, write a SQL query to find all dates' Ids with higher temperatur ...
- 大端(big endian) 小端(little endian) --- 在多字节存储 和 多字节通信中的含义(我还是太年轻了)
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ...
- clickhouse 安装和远程登录开启
一.Clickhouse的安装 1.添加yum源 yum-config-manager --add-repo http://repo.red-soft.biz/repos/clickhouse/rep ...
- Java课堂
import java.awt.*; import java.awt.event.*; import java.util.*; public class Main{ public static dou ...