翼度科技»论坛 编程开发 .net 查看内容

掌握 xUnit 单元测试中的 Mock 与 Stub 实战

2

主题

2

帖子

6

积分

新手上路

Rank: 1

积分
6
引言

上一章节介绍了 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仓储类库作为我们后续章节的演示项目
  1. dotNetParadise-Xunit
  2. ├── src
  3. │   ├── Sample.Api
  4. │   └── Sample.Repository
复制代码
Sample.Repository 是一个简单 EFCore 的仓储模式实现,Sample.Api 对外提供 RestFul 的 Api 接口
Sample.Repository 实现


  • 第一步 Sample.Repository类库安装 Nuget 包
  1. PM> NuGet\Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 8.0.3
复制代码
  1. PM> Microsoft.EntityFrameworkCore.Relational -Version 8.0.3
复制代码

  • 创建实体 Staff
  1. public class Staff
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Email { get; set; }
  6.     public int? Age { get; set; }
  7.     public List<string>? Addresses { get; set; }
  8.     public DateTimeOffset? Created { get; set; }
  9. }
复制代码

  • 创建 SampleDbContext 数据库上下文
  1. public class SampleDbContext(DbContextOptions<SampleDbContext> options) : DbContext(options)
  2. {
  3.     public DbSet<Staff> Staff { get; set; }
  4.     protected override void OnModelCreating(ModelBuilder builder)
  5.     {
  6.         base.OnModelCreating(builder);
  7.     }
  8. }
复制代码

  • 定义仓储接口和实现
  1. public interface IStaffRepository
  2. {
  3.     /// <summary>
  4.     /// 获取 Staff 实体的 DbSet
  5.     /// </summary>
  6.     DbSet<Staff> dbSet { get; }
  7.     /// <summary>
  8.     /// 添加新的 Staff 实体
  9.     /// </summary>
  10.     /// <param name="staff"></param>
  11.     Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default);
  12.     /// <summary>
  13.     /// 根据 Id 删除 Staff 实体
  14.     /// </summary>
  15.     /// <param name="id"></param>
  16.      Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default);
  17.     /// <summary>
  18.     /// 更新 Staff 实体
  19.     /// </summary>
  20.     /// <param name="staff"></param>
  21.     Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default);
  22.     /// <summary>
  23.     /// 根据 Id 获取单个 Staff 实体
  24.     /// </summary>
  25.     /// <param name="id"></param>
  26.     /// <returns></returns>
  27.     Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default);
  28.     /// <summary>
  29.     /// 获取所有 Staff 实体
  30.     /// </summary>
  31.     /// <returns></returns>
  32.     Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default);
  33.     /// <summary>
  34.     /// 批量更新 Staff 实体
  35.     /// </summary>
  36.     /// <param name="staffList"></param>
  37.     Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default);
  38. }
复制代码

  • 仓储实现
  1. public class StaffRepository : IStaffRepository
  2. {
  3.     private readonly SampleDbContext _dbContext;
  4.     public DbSet<Staff> dbSet => _dbContext.Set<Staff>();
  5.     public StaffRepository(SampleDbContext dbContext)
  6.     {
  7.         dbContext.Database.EnsureCreated();
  8.         _dbContext = dbContext;
  9.     }
  10.     public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default)
  11.     {
  12.         await dbSet.AddAsync(staff, cancellationToken);
  13.         await _dbContext.SaveChangesAsync(cancellationToken);
  14.     }
  15.     public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
  16.     {
  17.         //await dbSet.AsQueryable().Where(_ => _.Id == id).ExecuteDeleteAsync(cancellationToken);
  18.         var staff = await GetStaffByIdAsync(id, cancellationToken);
  19.         if (staff is not null)
  20.         {
  21.             dbSet.Remove(staff);
  22.             await _dbContext.SaveChangesAsync(cancellationToken);
  23.         }
  24.     }
  25.     public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default)
  26.     {
  27.         dbSet.Update(staff);
  28.         _dbContext.Entry(staff).State = EntityState.Modified;
  29.         await _dbContext.SaveChangesAsync(cancellationToken);
  30.     }
  31.     public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default)
  32.     {
  33.         return await dbSet.AsQueryable().Where(_ => _.Id == id).FirstOrDefaultAsync(cancellationToken);
  34.     }
  35.     public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default)
  36.     {
  37.         return await dbSet.ToListAsync(cancellationToken);
  38.     }
  39.     public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default)
  40.     {
  41.         await dbSet.AddRangeAsync(staffList, cancellationToken);
  42.         await _dbContext.SaveChangesAsync(cancellationToken);
  43.     }
  44. }
复制代码

  • 依赖注入
  1. public static class ServiceCollectionExtensions
  2. {
  3.     public static IServiceCollection AddEFCoreInMemoryAndRepository(this IServiceCollection services)
  4.     {
  5.         services.AddScoped<IStaffRepository, StaffRepository>();
  6.         services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("sample").EnableSensitiveDataLogging(), ServiceLifetime.Scoped);
  7.         return services;
  8.     }
  9. }
复制代码
到目前为止 仓储层的简单实现已经完成了,接下来完成 WebApi 层
Sample.Api

将 Sample.Api 添加项目引用Sample.Repository
program 依赖注入
  1. builder.Services.AddEFCoreInMemoryAndRepository();
复制代码

  • 定义 Controller
  1. [Route("api/[controller]")]
  2. [ApiController]
  3. public class StaffController(IStaffRepository staffRepository) : ControllerBase
  4. {
  5.     private readonly IStaffRepository _staffRepository = staffRepository;
  6.     [HttpPost]
  7.     public async Task<IResult> AddStaff([FromBody] Staff staff, CancellationToken cancellationToken = default)
  8.     {
  9.         await _staffRepository.AddStaffAsync(staff, cancellationToken);
  10.         return TypedResults.NoContent();
  11.     }
  12.     [HttpDelete("{id}")]
  13.     public async Task<IResult> DeleteStaff(int id, CancellationToken cancellationToken = default)
  14.     {
  15.         await _staffRepository.DeleteStaffAsync(id);
  16.         return TypedResults.NoContent();
  17.     }
  18.     [HttpPut("{id}")]
  19.     public async Task<Results<BadRequest<string>, NoContent, NotFound>> UpdateStaff(int id, [FromBody] Staff staff, CancellationToken cancellationToken = default)
  20.     {
  21.         if (id != staff.Id)
  22.         {
  23.             return TypedResults.BadRequest("Staff ID mismatch");
  24.         }
  25.         var originStaff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
  26.         if (originStaff is null) return TypedResults.NotFound();
  27.         originStaff.Update(staff);
  28.         await _staffRepository.UpdateStaffAsync(originStaff, cancellationToken);
  29.         return TypedResults.NoContent();
  30.     }
  31.     [HttpGet("{id}")]
  32.     public async Task<Results<Ok<Staff>, NotFound>> GetStaffById(int id, CancellationToken cancellationToken = default)
  33.     {
  34.         var staff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
  35.         if (staff == null)
  36.         {
  37.             return TypedResults.NotFound();
  38.         }
  39.         return TypedResults.Ok(staff);
  40.     }
  41.     [HttpGet]
  42.     public async Task<IResult> GetAllStaff(CancellationToken cancellationToken = default)
  43.     {
  44.         var staffList = await _staffRepository.GetAllStaffAsync(cancellationToken);
  45.         return TypedResults.Ok(staffList);
  46.     }
  47.     [HttpPost("BatchAdd")]
  48.     public async Task<IResult> BatchAddStaff([FromBody] List<Staff> staffList, CancellationToken cancellationToken = default)
  49.     {
  50.         await _staffRepository.BatchAddStaffAsync(staffList, cancellationToken);
  51.         return TypedResults.NoContent();
  52.     }
  53. }
复制代码
F5 项目跑一下

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

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

Stub 实战

Stub 是系统中现有依赖项的可控制替代项。通过使用 Stub,可以在测试代码时不需要使用真实依赖项。通常情况下,存根最初被视为 Fake
下面对 StaffController 利用 Stub 进行单元测试,

  • 创建一个 Stub 实现 IStaffRepository 接口,以模拟对数据库或其他数据源的访问操作。
  • 在单元测试中使用这个 Stub 替代 IStaffRepository 的实际实现,以便在不依赖真实数据源的情况下测试 StaffController 中的方法。
我们在dotNetParadise.FakeTest测试项目上新建一个IStaffRepository的实现,名字可以叫StubStaffRepository
  1. public class StubStaffRepository : IStaffRepository
  2. {
  3.     public DbSet<Staff> dbSet => default!;
  4.     public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken)
  5.     {
  6.         // 模拟添加员工操作
  7.         await Task.CompletedTask;
  8.     }
  9.     public async Task DeleteStaffAsync(int id)
  10.     {
  11.         // 模拟删除员工操作
  12.         await Task.CompletedTask;
  13.     }
  14.     public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken)
  15.     {
  16.         // 模拟更新员工操作
  17.         await Task.CompletedTask;
  18.     }
  19.     public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken)
  20.     {
  21.         // 模拟根据 ID 获取员工操作
  22.         return await Task.FromResult(new Staff { Id = id, Name = "Mock Staff" });
  23.     }
  24.     public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken)
  25.     {
  26.         // 模拟获取所有员工操作
  27.         return await Task.FromResult(new List<Staff> { new Staff { Id = 1, Name = "Mock Staff 1" }, new Staff { Id = 2, Name = "Mock Staff 2" } });
  28.     }
  29.     public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken)
  30.     {
  31.         // 模拟批量添加员工操作
  32.         await Task.CompletedTask;
  33.     }
  34.     public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
  35.     {
  36.         await Task.CompletedTask;
  37.     }
  38. }
复制代码
我们新创建了一个仓储的实现来替换StaffRepository作为新的依赖
下一步在单元测试项目测试我们的Controller方法
  1. public class TestStubStaffController
  2. {
  3.     [Fact]
  4.     public async Task AddStaff_WhenCalled_ReturnNoContent()
  5.     {
  6.         //Arrange
  7.         var staffController = new StaffController(new StubStaffRepository());
  8.         var staff = new Staff()
  9.         {
  10.             Age = 10,
  11.             Name = "Test",
  12.             Email = "Test@163.com",
  13.             Created = DateTimeOffset.Now,
  14.         };
  15.         //Act
  16.         var result = await staffController.AddStaff(staff);
  17.         //Assert
  18.         Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
  19.     }
  20.     [Fact]
  21.     public async Task GetStaffById_WhenCalled_ReturnOK()
  22.     {
  23.         //Arrange
  24.         var staffController = new StaffController(new StubStaffRepository());
  25.         var id = 1;
  26.         //Act
  27.         var result = await staffController.GetStaffById(id);
  28.         //Assert
  29.         Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
  30.         var okResult = (Ok<Staff>)result.Result;
  31.         Assert.Equal(id, okResult.Value?.Id);
  32.     }
  33.       //先暂时省略后面测试方法....
  34. }
复制代码

用 Stub 来替代真实的依赖项,以便更好地控制测试环境和测试结果
Mock

在测试过程中,尤其是TDD的开发过程中,测试用例有限开发在这个时候,我们总是要去模拟对象的创建,这些对象可能是某个接口的实现也可能是具体的某个对象,这时候就必须去写接口的实现,这时候模拟对象Mock的用处就体现出来了,在社区中也有很多模拟对象的库如Moq,FakeItEasy等。
Moq 是一个简单、直观且强大的.NET 模拟库,用于在单元测试中模拟对象和行为。通过 Moq,您可以轻松地设置依赖项的行为,并验证代码的调用。
我们用上面的实例来演示一下Moq的核心用法
第一步 Nuget 包安装Moq
  1. PM> NuGet\Install-Package Moq -Version 4.20.70
复制代码
您可以使用 Moq 中的 Setup 方法来设置模拟对象(Mock 对象)中可重写方法的行为,结合 Returns(用于返回一个值)或 Throws(用于抛出异常)等方法来定义其行为。这样可以模拟对特定方法的调用,使其在测试中返回预期的值或抛出特定的异常。
创建TestMockStaffController测试类,接下来我们用Moq实现一下上面的例子
  1. public class TestMockStaffController
  2. {
  3.     private readonly ITestOutputHelper _testOutputHelper;
  4.     public TestMockStaffController(ITestOutputHelper testOutputHelper)
  5.     {
  6.         _testOutputHelper = testOutputHelper;
  7.     }
  8.     [Fact]
  9.     public async Task AddStaff_WhenCalled_ReturnNoContent()
  10.     {
  11.         //Arrange
  12.         var mock = new Mock<IStaffRepository>();
  13.         mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
  14.         var staffController = new StaffController(mock.Object);
  15.         var staff = new Staff()
  16.         {
  17.             Age = 10,
  18.             Name = "Test",
  19.             Email = "Test@163.com",
  20.             Created = DateTimeOffset.Now,
  21.         };
  22.         //Act
  23.         var result = await staffController.AddStaff(staff);
  24.         //Assert
  25.         Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
  26.     }
  27.     [Fact]
  28.     public async Task GetStaffById_WhenCalled_ReturnOK()
  29.     {
  30.         //Arrange
  31.         var mock = new Mock<IStaffRepository>();
  32.         var id = 1;
  33.         mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).ReturnsAsync(() => new Staff()
  34.         {
  35.             Id = id,
  36.             Name = "张三",
  37.             Age = 18,
  38.             Email = "zhangsan@163.com",
  39.             Created = DateTimeOffset.Now
  40.         });
  41.         var staffController = new StaffController(mock.Object);
  42.         //Act
  43.         var result = await staffController.GetStaffById(id);
  44.         //Assert
  45.         Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
  46.         var okResult = (Ok<Staff>)result.Result;
  47.         Assert.Equal(id, okResult.Value?.Id);
  48.         _testOutputHelper.WriteLine(okResult.Value?.Name);
  49.     }
  50.     //先暂时省略后面测试方法....
  51. }
复制代码
看一下运行测试

Moq 核心功能讲解

通过我们上面这个简单的 Demo 简单的了解了一下 Moq 的使用,接下来我们对 Moq 和核心功能深入了解一下
通过安装的Nuget包可以看到, Moq依赖了Castle.Core这个包,Moq正是利用了 Castle 来实现动态代理模拟对象的功能。
基本概念


  • Mock 对象:通过 Moq 创建的模拟对象,用于模拟外部依赖项的行为。
    1. //创建Mock对象
    2. var mock = new Mock<IStaffRepository>();
    复制代码
  • Setup:用于设置 Mock 对象的行为和返回值,以指定当调用特定方法时应该返回什么结果。
    1. //指定调用AddStaffAsync方法的参数行为
    2.   mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
    复制代码
异步方法

从我们上面的单元测试中看到我们使用了一个异步方法,使用返回值ReturnsAsync表示的
  1.   mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
  2.        .ReturnsAsync(() => new Staff()
  3.         {
  4.             Id = id,
  5.             Name = "张三",
  6.             Age = 18,
  7.             Email = "zhangsan@163.com",
  8.             Created = DateTimeOffset.Now
  9.         });
复制代码
Moq有三种方式去设置异步方法的返回值分别是:

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

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

  • 使用 ReturnsAsync(较早版本):

    • 在较早版本的 Moq 中,您可以使用类似 ReturnsAsync、ThrowsAsync 等辅助方法来设置异步方法的返回值。
    • 示例:
      mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);

  • 使用 Lambda 表达式

    • 您还可以使用 Lambda 表达式来返回异步方法的结果。不过这种方式会触发有关异步 Lambda 同步执行的编译警告。
    • 示例:
      mock.Setup(foo => foo.DoSomethingAsync()).Returns(async () => 42);

参数匹配

在我们单元测试实例中用到了参数匹配,mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).,对就是这个It.IsAny(),此处的用意是匹配任意输入的 int类型的入参,接下来我们一起看下参数匹配的一些常用示例。

  • 任意值匹配
    It.IsAny()
    1. mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
    复制代码


  • ref 参数的任意值匹配:
    对于 ref 参数,可以使用 It.Ref.IsAny 进行匹配(需要 Moq 4.8 或更高版本)。
    1.        //Arrange
    2.      var mock = new Mock<IFoo>();
    3.      // ref arguments
    4.      var instance = new Bar();
    5.      // Only matches if the ref argument to the invocation is the same instance
    6.      mock.Setup(foo => foo.Submit(ref instance)).Returns(true);
    复制代码


  • 匹配满足条件的值:
    使用 It.Is(predicate) 可以匹配满足条件的值,其中 predicate 是一个函数。
    1.   //匹配满足条件的值
    2.   mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
    3. //It.Is 断言
    4. var result = mock.Object.Add(3);
    5. Assert.False(result);
    复制代码


  • 匹配范围:
    使用 It.IsInRange 可以匹配指定范围内的值
    1. mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Moq.Range.Inclusive))).Returns(true);
    2. var inRangeResult = mock.Object.Add(3);
    3. Assert.True(inRangeResult);
    复制代码


  • 匹配正则表达式:
    使用 It.IsRegex 可以匹配符合指定正则表达式的值
    1. {
    2.   mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo");
    3.   var result = mock.Object.DoSomethingStringy("a");
    4.   Assert.Equal("foo", result);
    5. }
    复制代码
属性值


  • 设置属性的返回值
    通过 Setup后的 Returns函数 设置Mock的返回值
    1. {
    2.   mock.Setup(foo => foo.Name).Returns("bar");
    3.   Assert.Equal("bar",mock.Object.Name);
    4. }
    复制代码


  • SetupSet 设置属性的设置行为,期望特定值被设置.
    主要是通过设置预期行为,对属性值做一些验证或者回调等操作
    1.   //SetupUp
    2.    mock = new Mock<IFoo>();
    3.    // Arrange
    4.    mock.SetupSet(foo => foo.Name = "foo").Verifiable();
    5.    //Act
    6.    mock.Object.Name = "foo";
    7.    mock.Verify();
    复制代码
如果值设置为mock.Object.Name = "foo1";,
单元测试就会抛出异常
OutPut:
  1.  dotNetParadise.FakeTest.TestControllers.TestMockStaffController.Test_Moq_Demo
  2.  源: TestMockStaffController.cs 行 70
  3.  持续时间: 8.7 秒
  4. 消息: 
  5. Moq.MockException : Mock<IFoo:2>:
  6. This mock failed verification due to the following:
  7. IFoo foo => foo.Name = "foo":
  8. This setup was not matched.
  9. 堆栈跟踪: 
  10. Mock.Verify(Func`2 predicate, HashSet`1 verifiedMocks) 行 309
  11. Mock.Verify() 行 251
  12. TestMockStaffController.Test_Moq_Demo() 行 111
  13. --- End of stack trace from previous location ---
复制代码

  • VerifySet 直接验证属性的设置操作
  1.        //VerifySet直接验证属性的设置操作
  2.        {
  3.            // Arrange
  4.            mock = new Mock<IFoo>();
  5.            //Act
  6.            mock.Object.Name = "foo";
  7.            //Asset
  8.            mock.VerifySet(person => person.Name = "foo");
  9.        }
复制代码

  • SetupProperty
    使用 SetupProperty 可以为 Mock 对象的属性设置行为,包括 get 和 set 的行为。
  1. {
  2.     // Arrange
  3.      mock = new Mock<IFoo>();
  4.       // start "tracking" sets/gets to this property
  5.      mock.SetupProperty(f => f.Name);
  6.       // alternatively, provide a default value for the stubbed property
  7.      mock.SetupProperty(f => f.Name, "foo");
  8.       //Now you can do:
  9.      IFoo foo = mock.Object;
  10.      // Initial value was stored
  11.      //Asset
  12.      Assert.Equal("foo", foo.Name);
  13. }
复制代码
在Moq 中,您可以使用 SetupAllProperties 方法来一次性存根(Stub)Mock 对象的所有属性。这意味着所有属性都会开始跟踪其值,并可以提供默认值。以下是一个示例演示如何使用 SetupAllProperties 方法:
  1. // 存根(Stub)Mock 对象的所有属性
  2. mock.SetupAllProperties();
复制代码
通过使用 SetupProperty 方法,可以更灵活地设置 Mock 对象的属性行为和默认值,以满足单元测试中的需求
处理事件(Events)

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

  • SetupAdd 用于设置 Mock 对象的事件的 add 访问器,即用于模拟事件订阅的行为


  • SetupRemove 用于设置 Mock 对象的事件的remove 访问器,以模拟事件处理程序的移除行为
创建要被测试的类:
  1. public class HasEvent
  2. {
  3.     public virtual event Action Event;
  4.     public void RaiseEvent() => this.Event?.Invoke();
  5. }
复制代码
  1.         {
  2.             var handled = false;
  3.             var mock = new Mock<HasEvent>();
  4.             //设置订阅行为
  5.             mock.SetupAdd(m => m.Event += It.IsAny<Action>()).CallBase();
  6.             // 订阅事件并设置事件处理逻辑
  7.             Action eventHandler = () => handled = true;
  8.             mock.Object.Event += eventHandler;
  9.             mock.Object.RaiseEvent();
  10.             Assert.True(handled);
  11.             // 重置标志为 false
  12.             handled = false;
  13.             //  移除事件处理程序
  14.             mock.SetupRemove(h => h.Event -= It.IsAny<Action>()).CallBase();
  15.             // 移除事件处理程序
  16.             mock.Object.Event -= eventHandler;
  17.             // 再次触发事件
  18.             mock.Object.RaiseEvent();
  19.             // Assert -  验证事件是否被正确处理
  20.             Assert.False(handled); // 第一次应该为 true,第二次应该为 false
  21.         }
复制代码
这段代码是一个针对 HasEvent 类的测试示例,使用 Moq 来设置事件的订阅和移除行为,并验证事件处理程序的添加和移除是否按预期工作。让我简单解释一下这段代码的流程:

  • 创建一个 Mock 对象 mock,模拟 HasEvent 类。
  • 使用 SetupAdd 方法设置事件的订阅行为,并使用 CallBase 方法调用基类的实现。
  • 订阅事件并设置事件处理逻辑,将事件处理程序 eventHandler 添加到事件中。
  • 调用 RaiseEvent 方法触发事件,并通过断言验证事件处理程序是否被正确处理。
  • 将 handled 标志重置为 false。
  • 使用 SetupRemove 方法设置事件的移除行为,并使用 CallBase 方法调用基类的实现。
  • 移除事件处理程序 eventHandler。
  • 再次触发事件,并通过断言验证事件处理程序是否被正确移除。
通过这个测试示例,可以验证事件处理程序的添加和移除操作是否正常工作


  • Raise
    Raise 方法用于手动触发 Mock 对象上的事件,模拟事件的触发过程
  1.         {
  2.             // Arrange
  3.             var handled = false;
  4.             var mock = new Mock<HasEvent>();
  5.             //设置订阅行为
  6.             mock.Object.Event += () => handled = true;
  7.             //act
  8.             mock.Raise(m => m.Event += null);
  9.             // Assert - 验证事件是否被正确处理
  10.             Assert.True(handled);
  11.         }
复制代码
这个示例使用Raise方法手动触发 Mock 对象上的事件 Event,并验证事件处理程序的执行情况。通过设置事件的订阅行为,触发事件,以及断言验证事件处理程序的执行结果,测试了事件处理程序的逻辑是否按预期执行。这个过程帮助我们确认事件处理程序在事件触发时能够正确执行.
Callbacks

Callback方法用于在设置 Mock 对象的成员时指定回调操作。当特定操作被调用时,可以在 Callback 方法中执行自定义的逻辑
  1.     //Arrange
  2.     var mock = new Mock<IFoo>();
  3.     var calls = 0;
  4.     var callArgs = new List<string>();
  5.     mock.Setup(foo => foo.DoSomething("ping"))
  6.         .Callback(() => calls++)
  7.        .Returns(true);
  8.     // Act
  9.     mock.Object.DoSomething("ping");
  10.     // Assert
  11.     Assert.Equal(1, calls); // 验证 DoSomething 方法被调用一次
复制代码
在调用 DoSomething 方法是,回调操作自动被触发参数++

  • CallBack 捕获参数
  1. //CallBack 捕获参数
  2. {
  3.      //Arrange
  4.      mock = new Mock<IFoo>();
  5.      mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
  6.          .Callback<string>(s => callArgs.Add(s))
  7.          .Returns(true);
  8.      //Act
  9.      mock.Object.DoSomething("a");
  10.      //Asset
  11.      // 验证参数是否被添加到 callArgs 列表中
  12.      Assert.Contains("a", callArgs);
  13. }
复制代码
使用 Moq 的 Callback 方法可以捕获方法调用时的参数,允许我们在测试中访问和处理这些参数。通过在 Setup 方法中指定 Callback 操作,我们可以捕获方法调用时传入的参数,并在回调中执行自定义逻辑,例如将参数添加到列表中。这种方法可以帮助我们验证方法在不同参数下的行为,以及检查方法是否被正确调用和传递参数。总的来说,Callback 方法为我们提供了一种灵活的方式来处理方法调用时的参数,帮助我们编写更全面的单元测试。


  • SetupProperty
    SetupProperty 方法可用于设置 Mock 对象的属性,并为其提供 getter 和 setter。
  1.         {
  2.             //Arrange
  3.             mock = new Mock<IFoo>();
  4.             mock.SetupProperty(foo => foo.Name);
  5.             mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
  6.                 .Callback((string s) => mock.Object.Name = s)
  7.                 .Returns(true);
  8.             //Act
  9.             mock.Object.DoSomething("a");
  10.             // Assert
  11.             Assert.Equal("a", mock.Object.Name);
  12.         }
复制代码
SetupProperty 方法的作用包括:

  • 设置属性的初始值:通过 SetupProperty 方法,我们可以设置 Mock 对象属性的初始值,使其在测试中具有特定的初始状态。
  • 模拟属性的 getter 和 setter:SetupProperty 方法允许我们为属性设置 getter 和 setter,使我们能够访问和修改属性的值。
  • 捕获属性的设置操作:在设置 Mock 对象的属性时,可以使用 Callback 方法捕获设置操作,以执行自定义逻辑或记录属性的设置情况。
  • 验证属性的行为:通过设置属性和相应的行为,可以验证属性的行为是否符合预期,以确保代码的正确性和可靠性
Verification

在 Moq 中,Verification 是指验证 Mock 对象上的方法是否被正确调用,以及调用时是否传入了预期的参数。通过 Verification,我们可以确保 Mock 对象的方法按预期进行了调用,从而验证代码的行为是否符合预期。
  1.         {
  2.             //Arrange
  3.             var mock = new Mock<IFoo>();
  4.             //Act
  5.             mock.Object.Add(1);
  6.             // Assert
  7.             mock.Verify(foo => foo.Add(1));
  8.         }
复制代码

  • 验证方法被调用的行为


  • 未被调用,或者调用至少一次
  1.    {
  2.        var mock = new Mock<IFoo>();
  3.        mock.Verify(foo => foo.DoSomething("ping"), Times.Never());
  4.    }
复制代码
  1. mock.Verify(foo => foo.DoSomething("ping"), Times.AtLeastOnce());
复制代码
Verify指定 Times.AtLeastOnce() 验证方法至少被调用了一次。

  • VerifySet
    验证是否是按续期设置,上面有讲过。


  • VerifyGet
    用于验证属性的 getter 方法至少被访问指定次数,或者没有被访问.
  1.     {
  2.         var mock = new Mock<IFoo>();
  3.          mock.VerifyGet(foo => foo.Name);
  4.     }
复制代码

  • VerifyAdd,VerifyRemove
VerifyAdd 和 VerifyRemove 方法来验证事件的订阅和移除
  1. // Verify event accessors (requires Moq 4.13 or later):
  2. mock.VerifyAdd(foo => foo.FooEvent += It.IsAny<EventHandler>());
  3. mock.VerifyRemove(foo => foo.FooEvent -= It.IsAny<EventHandler>());
复制代码

  • VerifyNoOtherCalls
VerifyNoOtherCalls 方法的作用是在使用 Moq 进行方法调用验证时,确保除了已经通过 Verify 方法验证过的方法调用外,没有其他未验证的方法被执行
  1. mock.VerifyNoOtherCalls();
复制代码
Customizing Mock Behavior


  • MockBehavior.Strict
    使用 Strict 模式创建的 Mock 对象时,如果发生了未设置期望的方法调用,包括未设置对方法的期望行为(如返回值、抛出异常等),则在该未设置期望的方法调用时会抛出 MockException 异常。这意味着在 Strict模式下,Mock 对象会严格要求所有的方法调用都必须有对应的期望设置,否则会触发异常。
  1.     [Fact]
  2.     public void TestStrictMockBehavior_WithUnsetExpectation()
  3.     {
  4.         // Arrange
  5.         var mock = new Mock<IFoo>(MockBehavior.Strict);
  6.         //mock.Setup(_ => _.Add(It.IsAny<int>())).Returns(true);
  7.         // Act & Assert
  8.         Assert.Throws<MockException>(() => mock.Object.Add(3));
  9.     }
复制代码
如果mock.Setup这一行注释了,即未设置期望值,则会抛出异常

  • CallBase
    在上面的示例中我们也能看到CallBase的使用
    在 Moq 中,通过设置 CallBase = true,可以创建一个部分模拟对象(Partial Mock),这样在没有设置期望的成员时,会调用基类的实现。这在需要模拟部分行为并保留基类实现的场景中很有用,特别适用于模拟 System.Web 中的 Web/Html 控件。
  1. public interface IUser
  2. {
  3.     string GetName();
  4. }
  5. public class UserBase : IUser
  6. {
  7.     public virtual string GetName()
  8.     {
  9.         return "BaseName";
  10.     }
  11.     string IUser.GetName() => "Name";
  12. }
复制代码
测试
  1.     [Fact]
  2.     public void TestPartialMockWithCallBase()
  3.     {
  4.         // Arrange
  5.        var mock = new Mock<UserBase> { CallBase = true };
  6.         mock.As<IUser>().Setup(foo => foo.GetName()).Returns("MockName");
  7.         // Act
  8.         string result = mock.Object.GetName();//
  9.         // Assert
  10.         Assert.Equal("BaseName", result);
  11.         //Act
  12.         var valueOfSetupMethod = ((IUser)mock.Object).GetName();
  13.         //Assert
  14.         Assert.Equal("MockName", valueOfSetupMethod);
  15.     }
复制代码
<ul>第一个Act:调用模拟对象的 GetName() 方法,此时基类的实现被调用,返回值为 "BaseName"。
第二个Act
来源:https://www.cnblogs.com/ruipeng/p/18130083
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具