引言

上一章节介绍了 TDD 的三大法则,今天我们讲一下在单元测试中模拟对象的使用。

Fake

Fake - Fake 是一个通用术语,可用于描述 stubmock 对象。 它是 stub 还是 mock 取决于使用它的上下文。 也就是说,Fake 可以是 stubmock

Mock - Mock 对象是系统中的 fake 对象,用于确定单元测试是否通过。 Mock 起初为 Fake,直到对其断言。

Stub - Stub 是系统中现有依赖项的可控制替代项。 通过使用 Stub,可以在无需使用依赖项的情况下直接测试代码。

参考 单元测试最佳做法 让我们使用相同的术语

区别点:

  1. Stub

    • 用于提供可控制的替代行为,通常是在测试中模拟依赖项的简单行为。
    • 主要用于提供固定的返回值或行为,以便测试代码的特定路径。
    • 不涉及对方法调用的验证,只是提供一个虚拟的实现。
  2. Mock
    • 用于验证方法的调用和行为,以确保代码按预期工作。
    • 主要用于确认特定方法是否被调用,以及被调用时的参数和次数。
    • 可以设置期望的调用顺序、参数和返回值,并在测试结束时验证这些调用。

总结:

  • Stub 更侧重于提供一个简单的替代品,帮助测试代码路径,而不涉及行为验证。
  • Mock 则更侧重于验证代码的行为和调用,以确保代码按预期执行。

在某些情况下两者可能看起来相似,但在测试的目的和用途上还是存在一些区别。在编写单元测试时,根据测试场景和需求选择合适的 stubmock对象可以帮助提高测试的准确性和可靠性。

创建实战项目

创建一个 WebApi Controller 项目,和一个EFCore仓储类库作为我们后续章节的演示项目

dotNetParadise-Xunit

├── src
│ ├── Sample.Api
│ └── Sample.Repository

Sample.Repository 是一个简单 EFCore 的仓储模式实现,Sample.Api 对外提供 RestFulApi 接口

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 项目跑一下

到这儿我们的项目已经创建完成了本系列后面的章节基本上都会以这个项目为基础展开拓展

控制器的单元测试

[单元测试涉及通过基础结构和依赖项单独测试应用的一部分。 单元测试控制器逻辑时,仅测试单个操作的内容,不测试其依赖项或框架自身的行为。

本章节主要以控制器的单元测试来带大家了解一下StupMoq的核心区别。

创建一个新的测试项目,然后添加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有三种方式去设置异步方法的返回值分别是:

  1. 使用 .Result 属性(Moq 4.16 及以上版本):

    • 在 Moq 4.16 及以上版本中,您可以直接通过 mock.Setup 返回任务的 .Result 属性来设置异步方法的返回值。这种方法几乎适用于所有设置和验证表达式。
    • 示例:

      mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
  2. 使用 ReturnsAsync(较早版本):

    • 在较早版本的 Moq 中,您可以使用类似 ReturnsAsyncThrowsAsync 等辅助方法来设置异步方法的返回值。
    • 示例:

      mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);
  3. 使用 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 对象的属性设置行为,包括 getset 的行为。
 {
// 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 方法来一次性存根(StubMock 对象的所有属性。这意味着所有属性都会开始跟踪其值,并可以提供默认值。以下是一个示例演示如何使用 SetupAllProperties 方法:

// 存根(Stub)Mock 对象的所有属性
mock.SetupAllProperties();

通过使用 SetupProperty 方法,可以更灵活地设置 Mock 对象的属性行为和默认值,以满足单元测试中的需求

处理事件(Events

Moq 4.13 及以后的版本中,你可以通过配置事件的 addremove 访问器来模拟事件的行为。这允许你指定当事件处理器被添加或移除时应该发生的逻辑。这通常用于验证事件是否被正确添加或移除,或者模拟事件触发时的行为。

  • 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 来设置事件的订阅和移除行为,并验证事件处理程序的添加和移除是否按预期工作。让我简单解释一下这段代码的流程:

  1. 创建一个 Mock 对象 mock,模拟 HasEvent 类。
  2. 使用 SetupAdd 方法设置事件的订阅行为,并使用 CallBase 方法调用基类的实现。
  3. 订阅事件并设置事件处理逻辑,将事件处理程序 eventHandler 添加到事件中。
  4. 调用 RaiseEvent 方法触发事件,并通过断言验证事件处理程序是否被正确处理。
  5. handled 标志重置为 false
  6. 使用 SetupRemove 方法设置事件的移除行为,并使用 CallBase 方法调用基类的实现。
  7. 移除事件处理程序 eventHandler
  8. 再次触发事件,并通过断言验证事件处理程序是否被正确移除。

通过这个测试示例,可以验证事件处理程序的添加和移除操作是否正常工作

  • 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);
}

使用 MoqCallback 方法可以捕获方法调用时的参数,允许我们在测试中访问和处理这些参数。通过在 Setup 方法中指定 Callback 操作,我们可以捕获方法调用时传入的参数,并在回调中执行自定义逻辑,例如将参数添加到列表中。这种方法可以帮助我们验证方法在不同参数下的行为,以及检查方法是否被正确调用和传递参数。总的来说,Callback 方法为我们提供了一种灵活的方式来处理方法调用时的参数,帮助我们编写更全面的单元测试。


  • SetupProperty

    SetupProperty 方法可用于设置 Mock 对象的属性,并为其提供 gettersetter
        {
//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 方法的作用包括:

  1. 设置属性的初始值:通过 SetupProperty 方法,我们可以设置 Mock 对象属性的初始值,使其在测试中具有特定的初始状态。

  2. 模拟属性的 getter 和 setterSetupProperty 方法允许我们为属性设置 gettersetter,使我们能够访问和修改属性的值。

  3. 捕获属性的设置操作:在设置 Mock 对象的属性时,可以使用 Callback 方法捕获设置操作,以执行自定义逻辑或记录属性的设置情况。

  4. 验证属性的行为:通过设置属性和相应的行为,可以验证属性的行为是否符合预期,以确保代码的正确性和可靠性

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

VerifyAddVerifyRemove 方法来验证事件的订阅和移除

// 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 接口具有一个虚拟属性 BarBar 类有一个虚拟方法 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.StrictDefaultValue.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 MocksMoq 提供的一种声明性规范查询方式,使得您可以通过 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")));

最后

这篇总结详细介绍了在单元测试中模拟对象的使用,包括 FakeMockStub 的概念及区别。针对 Moq 的核心功能进行了深入讲解,包括参数匹配、事件处理、回调操作、属性值设置、验证方法调用等内容。此外,还介绍了一些高级功能如自定义模拟对象行为、重置模拟对象、设置序列返回值、以及 LINQ to Mocks 的使用方式,后续章节开始我们的单元测试实战啦。

掌握 xUnit 单元测试中的 Mock 与 Stub 实战的更多相关文章

  1. 单元测试:单元测试中的mock

    公司要求提升单元测试的质量,提高代码的分支覆盖率和行覆盖率,安排我研究单元测试,指定方案分享并在开发部普及开.整理完资料后,同步一下到博客. 单元测试中的mock的目的 mock的主要目的是让单元测试 ...

  2. 单元测试中使用mock最好不要使用easymock而应该使用powermock

    视频参考汪文君powermock视频教程相当的经典

  3. 利用Python中的mock库对Python代码进行模拟测试

    这篇文章主要介绍了利用Python中的mock库对Python代码进行模拟测试,mock库自从Python3.3依赖成为了Python的内置库,本文也等于介绍了该库的用法,需要的朋友可以参考下     ...

  4. 【转】利用Python中的mock库对Python代码进行模拟测试

    出处 https://www.toptal.com/python/an-introduction-to-mocking-in-python http://www.oschina.net/transla ...

  5. 单元测试中使用Moq对EF的DbSet进行mock

    刚用上Moq,就用它解决了一个IUnitOfWork的mock问题,在这篇博文中记录一下. 开发场景 Application服务层BlogCategoryService的实现代码如下: public ...

  6. 单元测试Mockito中的Mock和Spy

    转载:https://blog.csdn.net/qq_30141957/article/details/81273829 项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能 ...

  7. 单元测试系列:Mock工具之Mockito实战

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...

  8. 单元测试系列:Mock工具Jmockit使用介绍

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...

  9. 单元测试系列之二:Mock工具Jmockit实战

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...

  10. [转]软件测试- 3 - Mock 和Stub的区别

    由于一直没有完全搞明白Mock和Stub的区别,所以查了很多文章,而这一篇是做好的: http://yuan.iteye.com/blog/470418 尤其是8楼,Frostred的发言,描述地相当 ...

随机推荐

  1. C++ //类模板分文件编写问题及解决 //第一中解决方式 直接包含源文件 //第二种解决方法 将.h 和 cpp的内容写到一起,将后缀改为.hpp文件

    1 //第一种方式被注释 2 //未被注释是第二种方式 3 //类模板分文件编写问题及解决 4 5 6 #include <iostream> 7 #include <string& ...

  2. ThreadLocal父子间通信的四种解决方案

    ThreadLocal父子间通信的四种解决方案 ThreadLocal 是存储在线程栈帧中的一块数据存储区域,其可以做到线程与线程之间的读写隔离. 但是在我们的日常场景中,经常会出现父线程需要向子线程 ...

  3. centos7 开机自动执行脚本

    1.因为在centos7中/etc/rc.d/rc.local的权限被降低了,所以需要赋予其可执行权 chmod +x /etc/rc.d/rc.local 2.赋予脚本可执行权限假设/usr/loc ...

  4. Linux性能监控(二)-top

    top命令可以用来监控服务器CPU.内存的运行情况,是Linux一个经常使用到的命令. 基本用法 第一行 显示当前系统运行信息,系统当前时间是23:23:21,运行了315days,当前有2个用户登录 ...

  5. 序列图 时序图 PlantUML vscode drawio 制作

    序列图 时序图 PlantUML vscode drawio 制作 需求 最近发现 序列图 很多文档都用到,而且很好用.经过研究用vscode,idea都可以编写.这里用vscode编写比较简单. d ...

  6. win10 vscode 设置 快捷键 ctrl + , ctrl + COMMA 与搜狗输入法 切换方法快捷键 冲突

    win10 vscode 快捷键 ctrl + , ctrl + COMMA 与搜狗输入法 切换方法快捷键 冲突 没想到是在系统里面 Step. 1: 选择切换语音 Step. 2: 选择 键盘 St ...

  7. [已读带总结] Effective JavaScript 编写高质量JavaScript代码的68个有效方法

    目录 电子书下载:https://www.jb51.net/books/328297.html 第2章 第11条 熟练掌握闭包 https://www.cnblogs.com/wengxuesong/ ...

  8. AAC编解码移植之基本简介

    一 概念 AAC是高级音频编码(Advanced Audio Coding)的缩写,出现于1997年,最初是基于MPEG-2的音频编码技术.由Fraunhofer IIS.Dolby Laborato ...

  9. 模板函数中的const

    所有讨论都是底层const指针或引用,顶层const不会传递进模板. 模板中有const,不管传进来是否是const,T都是非const类型. template<typename T> v ...

  10. Android富文本开发

    基础概念目录介绍 01.业务需求简单介绍 02.实现的方案介绍 03.异常状态下保存状态信息 04.处理软键盘回删按钮逻辑 05.在指定位置插入图片 06.在指定位置插入输入文字 07.如果对选中文字 ...