掌握 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
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...