引言 上一章節介紹了 TDD 的三大法則,今天我們講一下在單元測試中模擬對象的使用。 Fake Fake - Fake 是一個通用術語,可用於描述 stub或 mock 對象。 它是 stub 還是 mock 取決於使用它的上下文。 也就是說,Fake 可以是 stub 或 mock Mock - ...
引言
上一章節介紹了 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
倉儲類庫作為我們後續章節的演示項目
dotNetParadise-Xunit
│
├── src
│ ├── Sample.Api
│ └── Sample.Repository
Sample.Repository
是一個簡單 EFCore
的倉儲模式實現,Sample.Api
對外提供 RestFul
的 Api
介面
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
項目跑一下
到這兒我們的項目已經創建完成了本系列後面的章節基本上都會以這個項目為基礎展開拓展
控制器的單元測試
[單元測試涉及通過基礎結構和依賴項單獨測試應用的一部分。 單元測試控制器邏輯時,僅測試單個操作的內容,不測試其依賴項或框架自身的行為。
本章節主要以控制器的單元測試來帶大家瞭解一下Stup
和Moq
的核心區別。
創建一個新的測試項目,然後添加Sample.Api
的項目引用
Stub
實戰
Stub
是系統中現有依賴項的可控制替代項。通過使用 Stub
,可以在測試代碼時不需要使用真實依賴項。通常情況下,存根最初被視為 Fake
下麵對 StaffController
利用 Stub
進行單元測試,
- 創建一個
Stub
實現IStaffRepository
介面,以模擬對資料庫或其他數據源的訪問操作。 - 在單元測試中使用這個
Stub
替代IStaffRepository
的實際實現,以便在不依賴真實數據源的情況下測試StaffController
中的方法。
我們在dotNetParadise.FakeTest
測試項目上新建一個IStaffRepository
的實現,名字可以叫StubStaffRepository
public class StubStaffRepository : IStaffRepository
{
public DbSet<Staff> dbSet => default!;
public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken)
{
// 模擬添加員工操作
await Task.CompletedTask;
}
public async Task DeleteStaffAsync(int id)
{
// 模擬刪除員工操作
await Task.CompletedTask;
}
public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken)
{
// 模擬更新員工操作
await Task.CompletedTask;
}
public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken)
{
// 模擬根據 ID 獲取員工操作
return await Task.FromResult(new Staff { Id = id, Name = "Mock Staff" });
}
public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken)
{
// 模擬獲取所有員工操作
return await Task.FromResult(new List<Staff> { new Staff { Id = 1, Name = "Mock Staff 1" }, new Staff { Id = 2, Name = "Mock Staff 2" } });
}
public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken)
{
// 模擬批量添加員工操作
await Task.CompletedTask;
}
public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
}
}
我們新創建了一個倉儲的實現來替換StaffRepository
作為新的依賴
下一步在單元測試項目測試我們的Controller
方法
public class TestStubStaffController
{
[Fact]
public async Task AddStaff_WhenCalled_ReturnNoContent()
{
//Arrange
var staffController = new StaffController(new StubStaffRepository());
var staff = new Staff()
{
Age = 10,
Name = "Test",
Email = "[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);
}
//先暫時省略後面測試方法....
}
用
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);
}
//先暫時省略後面測試方法....
}
看一下運行測試
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
有三種方式去設置非同步方法的返回值分別是:
-
使用 .Result 屬性(Moq 4.16 及以上版本):
- 在 Moq 4.16 及以上版本中,您可以直接通過
mock.Setup
返回任務的.Result
屬性來設置非同步方法的返回值。這種方法幾乎適用於所有設置和驗證表達式。 - 示例:
mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
- 在 Moq 4.16 及以上版本中,您可以直接通過
-
使用 ReturnsAsync(較早版本):
- 在較早版本的 Moq 中,您可以使用類似
ReturnsAsync
、ThrowsAsync
等輔助方法來設置非同步方法的返回值。 - 示例:
mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);
- 在較早版本的 Moq 中,您可以使用類似
-
使用 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
對象的屬性設置行為,包括get
和set
的行為。
{
// 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
方法來一次性存根(Stub
)Mock
對象的所有屬性。這意味著所有屬性都會開始跟蹤其值,並可以提供預設值。以下是一個示例演示如何使用 SetupAllProperties
方法:
// 存根(Stub)Mock 對象的所有屬性
mock.SetupAllProperties();
通過使用 SetupProperty
方法,可以更靈活地設置 Mock 對象的屬性行為和預設值,以滿足單元測試中的需求
處理事件(Events
)
在 Moq
4.13 及以後的版本中,你可以通過配置事件的 add
和 remove
訪問器來模擬事件的行為。這允許你指定當事件處理器被添加或移除時應該發生的邏輯。這通常用於驗證事件是否被正確添加或移除,或者模擬事件觸發時的行為。
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 來設置事件的訂閱和移除行為,並驗證事件處理程式的添加和移除是否按預期工作。讓我簡單解釋一下這段代碼的流程:
- 創建一個 Mock 對象
mock
,模擬HasEvent
類。 - 使用
SetupAdd
方法設置事件的訂閱行為,並使用CallBase
方法調用基類的實現。 - 訂閱事件並設置事件處理邏輯,將事件處理程式
eventHandler
添加到事件中。 - 調用
RaiseEvent
方法觸發事件,並通過斷言驗證事件處理程式是否被正確處理。 - 將
handled
標誌重置為false
。 - 使用
SetupRemove
方法設置事件的移除行為,並使用CallBase
方法調用基類的實現。 - 移除事件處理程式
eventHandler
。 - 再次觸發事件,並通過斷言驗證事件處理程式是否被正確移除。
通過這個測試示例,可以驗證事件處理程式的添加和移除操作是否正常工作
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);
}
使用
Moq
的Callback
方法可以捕獲方法調用時的參數,允許我們在測試中訪問和處理這些參數。通過在Setup
方法中指定Callback
操作,我們可以捕獲方法調用時傳入的參數,併在回調中執行自定義邏輯,例如將參數添加到列表中。這種方法可以幫助我們驗證方法在不同參數下的行為,以及檢查方法是否被正確調用和傳遞參數。總的來說,Callback
方法為我們提供了一種靈活的方式來處理方法調用時的參數,幫助我們編寫更全面的單元測試。
SetupProperty
SetupProperty
方法可用於設置Mock
對象的屬性,併為其提供getter
和setter
。
{
//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
方法的作用包括:
-
設置屬性的初始值:通過
SetupProperty
方法,我們可以設置Mock
對象屬性的初始值,使其在測試中具有特定的初始狀態。 -
模擬屬性的 getter 和 setter:
SetupProperty
方法允許我們為屬性設置getter
和setter
,使我們能夠訪問和修改屬性的值。 -
捕獲屬性的設置操作:在設置
Mock
對象的屬性時,可以使用Callback
方法捕獲設置操作,以執行自定義邏輯或記錄屬性的設置情況。 -
驗證屬性的行為:通過設置屬性和相應的行為,可以驗證屬性的行為是否符合預期,以確保代碼的正確性和可靠性
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
VerifyAdd
和 VerifyRemove
方法來驗證事件的訂閱和移除
// 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