Mock 框架 Moq 的使用
Mock 框架 Moq 的使用
Intro
Moq 是 .NET 中一個很流行的 Mock 框架,使用 Mock 框架我們可以只針對我們關(guān)注的代碼進行測試,對于依賴項使用 Mock 對象配置預期的依賴服務(wù)的行為。
Moq 是基于 Castle 的動態(tài)代理來實現(xiàn)的,基于動態(tài)代理技術(shù)動態(tài)生成滿足指定行為的類型
在一個項目里, 我們經(jīng)常需要把某一部分程序獨立出來以便我們可以對這部分進行測試. 這就要求我們不要考慮項目其余部分的復雜性, 我們只想關(guān)注需要被測試的那部分. 這里就需要用到模擬(Mock)技術(shù).
因為, 請仔細看. 我們想要隔離測試的這部分代碼對外部有一個或者多個依賴. 所以編寫測試代碼的時候, 我們需要提供這些依賴. 而針對隔離測試, 并不應(yīng)該使用生產(chǎn)時用的依賴項, 所以我們使用模擬版本的依賴項, 這些模擬版依賴項只能用于測試時, 它們會使隔離更加容易.
img綠色的是需要被測試的類,黃色是Mock的依賴項
——引用自楊旭大佬的博文
Prepare
首先我們需要先準備一下用于測試的類和接口,下面的示例都是基于下面定義的類和方法來做的
public?interface?IUserIdProvider {string?GetUserId(); } public?class?TestModel {public?int?Id?{?get;?set;?} } public?interface?IRepository {int?Version?{?get;?set;?}int?GetCount();Task<int>?GetCountAsync();TestModel?GetById(int?id);List<TestModel>?GetList();TResult?GetResult<TResult>(string?sql);int?GetNum<T>();bool?Delete(int?id); }public?class?TestService {private?readonly?IRepository?_repository;public?TestService(IRepository?repository){_repository?=?repository;}public?int?Version{get?=>?_repository.Version;set?=>?_repository.Version?=?value;}public?List<TestModel>?GetList()?=>?_repository.GetList();public?TResult?GetResult<TResult>(string?sql)?=>?_repository.GetResult<TResult>(sql);public?int?GetResult(string?sql)?=>?_repository.GetResult<int>(sql);public?int?GetNum<T>()?=>?_repository.GetNum<T>();public?int?GetCount()?=>?_repository.GetCount();public?Task<int>?GetCountAsync()?=>?_repository.GetCountAsync();public?TestModel?GetById(int?id)?=>?_repository.GetById(id);public?bool?Delete(TestModel?model)?=>?_repository.Delete(model.Id); }我們要測試的類型就是類似 TestService 這樣的,而 IRepositoy<TestModel> 和 IUserIdProvider 是屬于外部依賴
Mock Method
Get Started
通常我們使用 Moq 最常用的可能就是 Mock 一個方法了,最簡單的一個示例如下:
[Fact] public?void?BasicTest() {var?userIdProviderMock?=?new?Mock<IUserIdProvider>();userIdProviderMock.Setup(x?=>?x.GetUserId()).Returns("mock");Assert.Equal("mock",?userIdProviderMock.Object.GetUserId()); }Match Arguments
通常我們的方法很多是帶有參數(shù)的,在使用 Moq 的時候我們可以通過設(shè)置參數(shù)匹配為不同的參數(shù)返回不同的結(jié)果,來看下面的這個例子:
[Fact] public?void?MethodParameterMatch() {var?repositoryMock?=?new?Mock<IRepository>();repositoryMock.Setup(x?=>?x.Delete(It.IsAny<int>())).Returns(true);repositoryMock.Setup(x?=>?x.GetById(It.Is<int>(_?=>?_?>?0))).Returns((int?id)?=>?new?TestModel(){Id?=?id});var?service?=?new?TestService(repositoryMock.Object);var?deleted?=?service.Delete(new?TestModel());Assert.True(deleted);var?result?=?service.GetById(1);Assert.NotNull(result);Assert.Equal(1,?result.Id);result?=?service.GetById(-1);Assert.Null(result);repositoryMock.Setup(x?=>?x.GetById(It.Is<int>(_?=>?_?<=?0))).Returns(()?=>?new?TestModel(){Id?=?-1});result?=?service.GetById(0);Assert.NotNull(result);Assert.Equal(-1,?result.Id); }通過 It.IsAny<T> 來表示匹配這個類型的所有值,通過 It.Is<T>(Expression<Func<bool>>) 來設(shè)置一個表達式來斷言這個類型的值
通過上面的例子,我們可以看的出來,設(shè)置返回值的時候,可以直接設(shè)置一個固定的返回值,也可以設(shè)置一個委托來返回一個值,也可以根據(jù)方法的參數(shù)來動態(tài)配置返回結(jié)果
Async Method
現(xiàn)在很多地方都是在用異步方法,Moq 設(shè)置異步方法有三種方式,一起來看一下示例:
[Fact] public?async?Task?AsyncMethod() {var?repositoryMock?=?new?Mock<IRepository>();//?Task.FromResultrepositoryMock.Setup(x?=>?x.GetCountAsync()).Returns(Task.FromResult(10));//?ReturnAsyncrepositoryMock.Setup(x?=>?x.GetCountAsync()).ReturnsAsync(10);//?Mock?Result,?start?from?4.16repositoryMock.Setup(x?=>?x.GetCountAsync().Result).Returns(10);var?service?=?new?TestService(repositoryMock.Object);var?result?=?await?service.GetCountAsync();Assert.True(result?>?0); }還有一個方式也可以,但是不推薦,編譯器也會給出一個警告,就是下面這樣
repositoryMock.Setup(x?=>?x.GetCountAsync()).Returns(async?()?=>?10);Generic Type
有些方法會是泛型方法,對于泛型方法,我們來看下面的示例:
[Fact] public?void?GenericType() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.Setup(x?=>?x.GetResult<int>(It.IsAny<string>())).Returns(1);Assert.Equal(1,?service.GetResult(""));repositoryMock.Setup(x?=>?x.GetResult<string>(It.IsAny<string>())).Returns("test");Assert.Equal("test",?service.GetResult<string>("")); }[Fact] public?void?GenericTypeMatch() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.Setup(m?=>?m.GetNum<It.IsAnyType>()).Returns(-1);repositoryMock.Setup(m?=>?m.GetNum<It.IsSubtype<TestModel>>()).Returns(0);repositoryMock.Setup(m?=>?m.GetNum<string>()).Returns(1);repositoryMock.Setup(m?=>?m.GetNum<int>()).Returns(2);Assert.Equal(0,?service.GetNum<TestModel>());Assert.Equal(1,?service.GetNum<string>());Assert.Equal(2,?service.GetNum<int>());Assert.Equal(-1,?service.GetNum<byte>()); }如果要 Mock 指定類型的數(shù)據(jù),可以直接指定泛型類型,如上面的第一個測試用例,如果要不同類型設(shè)置不同的結(jié)果一種是直接設(shè)置類型,如果要指定某個類型或者某個類型的子類,可以用 It.IsSubtype<T>,如果要指定值類型可以用 It.IsValueType,如果要匹配所有類型則可以用 It.IsAnyType
Callback
我們在設(shè)置 Mock 行為的時候可以設(shè)置 callback 來模擬方法執(zhí)行時的邏輯,來看一下下面的示例:
[Fact] public?void?Callback() {var?deletedIds?=?new?List<int>();var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.Setup(x?=>?x.Delete(It.IsAny<int>())).Callback((int?id)?=>{deletedIds.Add(id);}).Returns(true);for?(var?i?=?0;?i?<?10;?i++){service.Delete(new?TestModel()?{?Id?=?i?});}Assert.Equal(10,?deletedIds.Count);for?(var?i?=?0;?i?<?10;?i++){Assert.Equal(i,?deletedIds[i]);} }Verification
有時候我們會驗證某個方法是否執(zhí)行,并不需要關(guān)注是否方法的返回值,這時我們可以使用 Verification 驗證某個方法是否被調(diào)用,示例如下:
[Fact] public?void?Verification() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);service.Delete(new?TestModel(){Id?=?1});repositoryMock.Verify(x?=>?x.Delete(1));repositoryMock.Verify(x?=>?x.Version,?Times.Never());Assert.Throws<MockException>(()?=>?repositoryMock.Verify(x?=>?x.Delete(2))); }如果方法沒有被調(diào)用,就會引發(fā)一個 MockException 異常:
verification failedVerification 也可以指定方法觸發(fā)的次數(shù),比如:repositoryMock.Verify(x => x.Version, Times.Never);,默認是 Times.AtLeastOnce,可以指定具體次數(shù) Times.Exactly(1) 或者指定一個范圍 Times.Between(1,2, Range.Inclusive),Moq 也提供了一些比較方便的方法,比如Times.Never()/Times.Once()/Times.AtLeaseOnce()/Times.AtMostOnce()/Times.AtLease(2)/Times.AtMost(2)
Mock Property
Moq 也可以 mock 屬性,property 的本質(zhì)是方法加一個字段,所以也可以用 Mock 方法的方式來 Mock 屬性,只是使用 Mock 方法的方式進行 Mock 屬性的話,后續(xù)修改屬性值就不會引起屬性值的變化了,如果修改屬性,則要使用 SetupProperty 的方式來 Mock 屬性,具體可以參考下面的這個示例:
[Fact] public?void?Property() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.Setup(x?=>?x.Version).Returns(1);Assert.Equal(1,?service.Version);service.Version?=?2;Assert.Equal(1,?service.Version); }[Fact] public?void?PropertyTracking() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.SetupProperty(x?=>?x.Version,?1);Assert.Equal(1,?service.Version);service.Version?=?2;Assert.Equal(2,?service.Version); }Sequence
我們可以通過 Sequence 來指定一個方法執(zhí)行多次返回不同結(jié)果的效果,看一下示例就明白了:
[Fact] public?void?Sequence() {var?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);repositoryMock.SetupSequence(x?=>?x.GetCount()).Returns(1).Returns(2).Returns(3).Throws(new?InvalidOperationException());Assert.Equal(1,?service.GetCount());Assert.Equal(2,?service.GetCount());Assert.Equal(3,?service.GetCount());Assert.Throws<InvalidOperationException>(()?=>?service.GetCount()); }第一次調(diào)用返回值是1,第二次是2,第三次是3,第四次是拋了一個 InvalidOperationException
LINQ to Mocks
我們可以通過 Mock.Of 來實現(xiàn)類似 LINQ 的方式,創(chuàng)建一個 mock 對象實例,指定類型的實例,如果對象比較深,要 mock 的對象比較多使用這種方式可能會一定程度上簡化自己的代碼,來看使用示例:
[Fact] public?void?MockLinq() {var?services?=?Mock.Of<IServiceProvider>(sp?=>sp.GetService(typeof(IRepository))?==?Mock.Of<IRepository>(r?=>?r.Version?==?1)?&&sp.GetService(typeof(IUserIdProvider))?==?Mock.Of<IUserIdProvider>(a?=>?a.GetUserId()?==?"test"));Assert.Equal(1,?services.ResolveService<IRepository>().Version);Assert.Equal("test",?services.ResolveService<IUserIdProvider>().GetUserId()); }Mock Behavior
默認的 Mock Behavior 是 Loose,默認沒有設(shè)置預期行為的時候不會拋異常,會返回方法返回值類型的默認值或者空數(shù)組或者空枚舉,
在聲明 Mock 對象的時候可以指定 Behavior 為 Strict,這樣就是一個**"真正"**的 mock 對象,沒有設(shè)置預期行為的時候就會拋出異常,示例如下:
[Fact] public?void?MockBehaviorTest() {//?Make?mock?behave?like?a?"true?Mock",//?raising?exceptions?for?anything?that?doesn't?have?a?corresponding?expectation:?in?Moq?slang?a?"Strict"?mock;//?default?behavior?is?"Loose"?mock,//?which?never?throws?and?returns?default?values?or?empty?arrays,?enumerable,?etcvar?repositoryMock?=?new?Mock<IRepository>();var?service?=?new?TestService(repositoryMock.Object);Assert.Equal(0,?service.GetCount());Assert.Null(service.GetList());var?arrayResult?=?repositoryMock.Object.GetArray();Assert.NotNull(arrayResult);Assert.Empty(arrayResult);repositoryMock?=?new?Mock<IRepository>(MockBehavior.Strict);Assert.Throws<MockException>(()?=>?new?TestService(repositoryMock.Object).GetCount()); }使用 Strict 模式不設(shè)置預期行為的時候就會報異常,異常信息類似下面這樣:
strict exceptionMore
Moq 還有一些別的用法,還支持事件的操作,還有 Protected 成員的 Mock,還有一些高級的用法,自定義 Default 行為等,感覺我們平時可能并不太常用,所以上面并沒有加以介紹,有需要用的可以參考 Moq 的文檔
上述測試代碼可以在 Github 獲取 https://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs
References
https://github.com/moq/moq4/wiki/Quickstart
https://github.com/moq/moq4
https://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs
https://www.cnblogs.com/tylerzhou/p/11410337.html
https://www.cnblogs.com/cgzl/p/9304567.html
https://www.cnblogs.com/haogj/archive/2011/07/22/2113496.html
總結(jié)
以上是生活随笔為你收集整理的Mock 框架 Moq 的使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在 ASP.NET Core MVC
- 下一篇: 聊一聊和Nacos 2.0.0对接那些事