掌握 xUnit 單元測試中的 Mock 與 Stub 實戰

来源:https://www.cnblogs.com/ruipeng/p/18130083
-Advertisement-
Play Games

引言 上一章節介紹了 TDD 的三大法則,今天我們講一下在單元測試中模擬對象的使用。 Fake Fake - Fake 是一個通用術語,可用於描述 stub或 mock 對象。 它是 stub 還是 mock 取決於使用它的上下文。 也就是說,Fake 可以是 stub 或 mock Mock - ...


引言

上一章節介紹了 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 項目跑一下

image

到這兒我們的項目已經創建完成了本系列後面的章節基本上都會以這個項目為基礎展開拓展

控制器的單元測試

[單元測試涉及通過基礎結構和依賴項單獨測試應用的一部分。 單元測試控制器邏輯時,僅測試單個操作的內容,不測試其依賴項或框架自身的行為。

本章節主要以控制器的單元測試來帶大家瞭解一下StupMoq的核心區別。

創建一個新的測試項目,然後添加Sample.Api的項目引用

image

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 = "[email protected]",
            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);
    }

      //先暫時省略後面測試方法....

}

image

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 = "[email protected]",
            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 = "[email protected]",
            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);

    }

    //先暫時省略後面測試方法....
}

看一下運行測試

image

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 = "[email protected]",
            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
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文深入探討了Kubernetes Pod配置的實戰技巧和常見易錯點。 關註【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩,復旦機器人智能實驗室成員,阿裡雲認證的資深架構師,項目管理專業人士,上億營 ...
  • 本文分享自華為雲社區《Python 正則表達式大揭秘應用與技巧全解析》,作者:檸檬味擁抱。 Python 中的 re 模塊是用於處理正則表達式的強大工具。正則表達式是一種用來匹配字元串的模式,它可以在文本中搜索和匹配特定的字元串模式。在本文中,我們將探討 Python 中 re 模塊的應用和一些技巧 ...
  • 寫在前面 一款好的插件往往能提高我們的開發效率。今天就給大家安利一款maven 依賴搜索插件。 插件是自己一直關註的魯班大叔開發的,用了幾天真的好用 廢話不多說,我們就來看看這是一款什麼插件 一、maven 依賴搜索 平常我們需要找一個maven依賴,一般都會去 https://mvnreposit ...
  • OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。 要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪 ...
  • 1 開源解析和拆分文檔 第三方的工具去對文件解析拆分,去將我們的文件內容給提取出來,並將我們的文檔內容去拆分成一個小的chunk。常見的PDF word mark down, JSON、HTML。都可以有很好的一些模塊去把這些文件去進行一個東西去提取。 優勢 支持豐富的文檔類型 每種文檔多樣化選擇 ...
  • LiteDB 是一個輕量級的嵌入式 NoSQL 資料庫,其設計理念與 MongoDB 類似,但它是完全使用 C# 開發的,因此與 C# 應用程式的集成非常順暢。與 SQLite 相比,LiteDB 提供了 NoSQL(即鍵值對)的數據存儲方式,並且是一個開源且免費的項目。它適用於桌面、移動以及 We ...
  • 經過前面幾篇的學習,我們瞭解到指令的大概分類,如:參數載入指令,該載入指令以 Ld 開頭,將參數載入到棧中,以便於後續執行操作命令。參數存儲指令,其指令以 St 開頭,將棧中的數據,存儲到指定的變數中,以方便後續使用。創建實例指令,其指令以 New 開頭,用於在運行時動態生成並初始化對象。方法調用指... ...
  • 為.net6在CentOS7上面做準備,先在vmware虛擬機安裝CentOS 7.9 新建CentOS764位的系統 因為CentOS8不更新了,所以安裝7;簡單就一筆帶過了 選擇下載好的操作系統的iso文件,下載地址https://mirrors.aliyun.com/centos/7.9.20 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 推薦一款基於.NET 8、WPF、Prism.DryIoc、MVVM設計模式、Blazor以及MySQL資料庫構建的企業級工作流系統的WPF客戶端框架-AIStudio.Wpf.AClient 6.0。 項目介紹 框架採用了 Prism 框架來實現 MVVM 模式,不僅簡化了 MVVM 的典型 ...
  • 先看一下效果吧: 我們直接通過改造一下原版的TreeView來實現上面這個效果 我們先創建一個普通的TreeView 代碼很簡單: <TreeView> <TreeViewItem Header="人事部"/> <TreeViewItem Header="技術部"> <TreeViewItem He ...
  • 1. 生成式 AI 簡介 https://imp.i384100.net/LXYmq3 2. Python 語言 https://imp.i384100.net/5gmXXo 3. 統計和 R https://youtu.be/ANMuuq502rE?si=hw9GT6JVzMhRvBbF 4. 數 ...
  • 本文為大家介紹下.NET解壓/壓縮zip文件。雖然解壓縮不是啥核心技術,但壓縮性能以及進度處理還是需要關註下,針對使用較多的zip開源組件驗證,給大家提供個技術選型參考 之前在《.NET WebSocket高併發通信阻塞問題 - 唐宋元明清2188 - 博客園 (cnblogs.com)》講過,團隊 ...
  • 之前寫過兩篇關於Roslyn源生成器生成源代碼的用例,今天使用Roslyn的代碼修複器CodeFixProvider實現一個cs文件頭部註釋的功能, 代碼修複器會同時涉及到CodeFixProvider和DiagnosticAnalyzer, 實現FileHeaderAnalyzer 首先我們知道修 ...
  • 在軟體行業,經常會聽到一句話“文不如表,表不如圖”說明瞭圖形在軟體應用中的重要性。同樣在WPF開發中,為了程式美觀或者業務需要,經常會用到各種個樣的圖形。今天以一些簡單的小例子,簡述WPF開發中幾何圖形(Geometry)相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 在 C# 中使用 RabbitMQ 通過簡訊發送重置後的密碼到用戶的手機號上,你可以按照以下步驟進行 1.安裝 RabbitMQ 客戶端庫 首先,確保你已經安裝了 RabbitMQ 客戶端庫。你可以通過 NuGet 包管理器來安裝: dotnet add package RabbitMQ.Clien ...
  • 1.下載 Protocol Buffers 編譯器(protoc) 前往 Protocol Buffers GitHub Releases 頁面。在 "Assets" 下找到適合您系統的壓縮文件,通常為 protoc-{version}-win32.zip 或 protoc-{version}-wi ...
  • 簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...