掌握 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
  • 不廢話,直接代碼 private Stack<Action> actionStack = new Stack<Action>(); private void SetCellValues() { var worksheet = Globals.ThisAddIn.Application.ActiveS ...
  • OpenAPI 規範是用於描述 HTTP API 的標準。該標準允許開發人員定義 API 的形狀,這些 API 可以插入到客戶端生成器、伺服器生成器、測試工具、文檔等中。儘管該標準具有普遍性和普遍性,但 ASP.NET Core 在框架內預設不提供對 OpenAPI 的支持。 當前 ASP.NET ...
  • @DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用於處理日期時間格式的註解,它們有不同的作用: @DateTimeFormat @DateTimeFormat 是 Spring 框架提供的註解,用於指定字元串如何轉換為日期時間類型,以及如何格式化日 ...
  • 一、背景說明 1.1 效果演示 用python開發的爬蟲採集軟體,可自動抓取抖音評論數據,並且含二級評論! 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python、無需懂代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 以上。 1.2 演示視頻 軟體運行演示視頻:見 ...
  • SpringBoot筆記 SpringBoot文檔 官網: https://spring.io/projects/spring-boot 學習文檔: https://docs.spring.io/spring-boot/docs/current/reference/html/ 線上API: http ...
  • 作為後端工程師,多數情況都是給別人提供介面,寫的好不好使你得重視起來。 最近我手頭一些活,需要和外部公司對接,我們需要提供一個介面文檔,這樣可以節省雙方時間、也可以防止後續扯皮。這是就要考驗我的介面是否規範化。 1. 介面名稱清晰、明確 顧名思義,介面是做什麼的,是否準確、清晰?讓使用這一眼就能知道 ...
  • 本文介紹基於Python語言,遍歷文件夾並從中找到文件名稱符合我們需求的多個.txt格式文本文件,並從上述每一個文本文件中,找到我們需要的指定數據,最後得到所有文本文件中我們需要的數據的合集的方法~ ...
  • Java JUC&多線程 基礎完整版 目錄Java JUC&多線程 基礎完整版1、 多線程的第一種啟動方式之繼承Thread類2、多線程的第二種啟動方式之實現Runnable介面3、多線程的第三種實現方式之實現Callable介面4、多線的常用成員方法5、線程的優先順序6、守護線程7、線程的讓出8、線 ...
  • 實時識別關鍵詞是一種能夠將搜索結果提升至新的高度的API介面。它可以幫助我們更有效地分析文本,並提取出關鍵詞,以便進行進一步的處理和分析。 該介面是挖數據平臺提供的,有三種模式:精確模式、全模式和搜索引擎模式。不同的模式在分詞的方式上有所不同,適用於不同的場景。 首先是精確模式。這種模式會儘量將句子 ...
  • 1 為啥要折騰搭建一個專屬圖床? 技術大佬寫博客都用 md 格式,要在多平臺發佈,圖片就得有外鏈 後續如博客遷移,國內博客網站如掘金,簡書,語雀等都做了防盜鏈,圖片無法遷移 2 為啥選擇CloudFlare R2 跳轉:https://dash.cloudflare.com/ 有白嫖額度 免費 CD ...