.net测试篇之Moq框架简单使用
Moq簡(jiǎn)介
Moq是.net平臺(tái)下的一個(gè)非常流行的模擬庫(kù),只要有一個(gè)接口它就可以動(dòng)態(tài)生成一個(gè)對(duì)象,底層使用的是Castle的動(dòng)態(tài)代理功能.
它的流行賴于依賴注入模式的興起,現(xiàn)在越來(lái)越多的分層架構(gòu)使用依賴注入的方式來(lái)解耦層與層之間的關(guān)系.最為常見的是數(shù)據(jù)層和業(yè)務(wù)邏輯層之間的依賴注入,業(yè)務(wù)邏輯層不再?gòu)?qiáng)依賴數(shù)據(jù)層對(duì)象,而是依賴數(shù)據(jù)層對(duì)象的接口,在IOC容器里完成依賴的配置.
這種解耦給單元測(cè)試帶來(lái)了巨大的便利,使得對(duì)業(yè)務(wù)邏輯的測(cè)試可以脫離對(duì)數(shù)據(jù)層的依賴,單元測(cè)試的粒度更小,更容易排查出問(wèn)題所在.
大家可能都知道,數(shù)據(jù)層的接口往往有很多方法,少則十幾個(gè),多則幾十個(gè).我們?nèi)绻趩卧獪y(cè)試的時(shí)候把接口切換為假實(shí)現(xiàn),即使實(shí)現(xiàn)類全是空也需要大量代碼,并且這些代碼不可重用,一旦接口層改變不但要更改真實(shí)數(shù)據(jù)層實(shí)現(xiàn)還要修改這些專為測(cè)試做的假實(shí)現(xiàn).這顯然是不小的工作量.
幸好有Moq,它可以在編譯時(shí)動(dòng)態(tài)生成接口的代理對(duì)象.大大提高了代碼的可維護(hù)性,同時(shí)也極大減少工作量.
除了動(dòng)態(tài)創(chuàng)建代理外,Moq還可以進(jìn)行行為測(cè)試,觸發(fā)事件等.
Moq安裝
Moq安裝非常簡(jiǎn)單,在Nuget里面搜索moq,第一個(gè)結(jié)果便是moq框架,點(diǎn)擊安裝即可.
本示例中要使用到的代碼如下
public class MyDto{public string Name { get; set; }public int Age { get; set; }}public interface IDataBaseContext<out T> where T:new(){T GetElementById(string id);IEnumerable<T> GetAll();IEnumerable<T> GetElementsByName(string name);IEnumerable<T> GetPageElementsByName(string name, int startPage = 0, int pageSize = 20);IEnumerable<T> GetElementsByDate(DateTime? startDate, DateTime? endDate);}public class MyBll{private readonly IDataBaseContext<MyDto> _dataBaseContext;public MyBll(IDataBaseContext<MyDto> dataBaseContext){_dataBaseContext = dataBaseContext;}public MyDto GetADto(string id){if (string.IsNullOrWhiteSpace(id)) return null;return _dataBaseContext.GetElementById(id);}}MyDto為業(yè)務(wù)層和數(shù)據(jù)層交互的對(duì)象,IDataBaseContext為數(shù)據(jù)層接口,MyBll為我們的業(yè)務(wù)邏輯層
我們要測(cè)試的是業(yè)務(wù)邏輯層的代碼.這里業(yè)務(wù)邏輯類并沒有無(wú)參構(gòu)造函數(shù),如果手動(dòng)創(chuàng)建起來(lái)非常麻煩,里面的坑前面說(shuō)過(guò).下面看如何使用Moq來(lái)模擬一個(gè)IDataBaseContext對(duì)象
我們編寫以下測(cè)試類
[Test]public void SimpleTest(){var moq = new Mock<IDataBaseContext<MyDto>>();MyBll bll = new MyBll(moq.Object);var result = bll.GetADto(null);Assert.Null(result);}由于bll的GetADto如果傳的參數(shù)是null或者空就會(huì)返回一個(gè)null對(duì)象,因些返回的結(jié)果是Null,以上測(cè)試會(huì)通過(guò).
這里我們首先創(chuàng)建了一個(gè)moq對(duì)象,它的Object屬性就是我們要模擬的IDataBaseContext對(duì)象,我們?cè)趧?chuàng)建MyBll對(duì)象時(shí)把它作為參數(shù)傳入.
我們?cè)贋镸yBll添加以下方法
public IEnumerable<MyDto> GetDtos(string name){if (string.IsNullOrWhiteSpace(name)) return null;var dtos = _dataBaseContext.GetElementsByName(name);return dtos;}我們編寫如下測(cè)試方法
[Test]public void ShouldReturn_A_Collection_Of_Dtos(){var moq = new Mock<IDataBaseContext<MyDto>>();MyBll bll = new MyBll(moq.Object);var dtos = bll.GetDtos("sto");}以上測(cè)試方法調(diào)用了bll的GetDtos方法,我們知道GetDtos內(nèi)部調(diào)用了數(shù)據(jù)訪問(wèn)接口的GetElementsByName方法,我們?cè)谡{(diào)試模式下看看返回的結(jié)果是什么.
它返回了一個(gè)空集合,實(shí)際上不管我們提供的是什么樣的字符串,它都返回一個(gè)空集合,這是默認(rèn)行為,因?yàn)開dataBaseContext.GetElementsByName并不知道我們的真實(shí)邏輯是什么.
這樣很顯然并不是總能滿足我們的要求,很多時(shí)候我們?cè)跍y(cè)試業(yè)務(wù)邏輯層的時(shí)候需要具體的數(shù)據(jù),然后才能繼續(xù)往下走.
比如以下方法,我們獲取數(shù)據(jù)庫(kù)里的所有數(shù)據(jù),然而通過(guò)一系列邏輯進(jìn)行過(guò)濾,最終返回過(guò)濾后的結(jié)果.
public IEnumerable<MyDto> GetAllDtos(){var all = _dataBaseContext.GetAll().ToList();if (!all.Any()) return Enumerable.Empty<MyDto>();var filteredDtos = all.Where(a => a.Age > 20);var orderDtos = filteredDtos.OrderBy(a => a.Name);return orderDtos;}如果是默認(rèn)行為(調(diào)用模擬的接口方法,引用對(duì)象返回null,集合返回空,簡(jiǎn)單對(duì)象返回默認(rèn)值),則代碼很快就返回了,if下面的業(yè)務(wù)邏輯測(cè)不到了.下面我們看下如何配置接口方法的返回值
這里其實(shí)主要用到了 新建moq對(duì)象的setup方法,我們可以在setup里設(shè)置方法,屬性的值.
[Test]public void ShouldReturn_A_Collection_Of_Dtos(){var moq = new Mock<IDataBaseContext<MyDto>>();moq.Setup(a => a.GetAll()).Returns(new List<MyDto>{new MyDto{Name="baidu",Age=15},new MyDto{Name="sto",Age=32},new MyDto{Name="zto",Age=24},new MyDto{Name="yto",Age=12}});MyBll bll = new MyBll(moq.Object);var dtos = bll.GetAllDtos().ToList();dtos.Should().HaveCount(2);dtos.Select(a => a.Name).Should().BeInAscendingOrder();}我們看以上代碼,我們我們讓數(shù)據(jù)訪問(wèn)接口的代理對(duì)象返回一個(gè)MyDto類型集合,一共四個(gè)元素,由我們的業(yè)務(wù)可知,我們只要年齡大于20的元素,并且名字按正序排列.因此以上測(cè)試應(yīng)該返回成功,實(shí)際上也是測(cè)試通過(guò)了.
帶參數(shù)的方法設(shè)置
以上的GetAll是不帶參數(shù)的,帶參數(shù)的方法我們可以顯式的指定一個(gè)參數(shù),我們也可以使用Moq框架提供的方法來(lái)模糊指定參數(shù),比如我們可以指定方法是任意字符,任意數(shù)字,任意范圍的數(shù)字等.
我們?cè)倏辞懊娴囊粋€(gè)方法
public MyDto GetADto(string id){if (string.IsNullOrWhiteSpace(id)) return null;return _dataBaseContext.GetElementById(id);}這個(gè)方法接收一個(gè)類型為字符串的id,只要字符串不是空字符串或者null時(shí)我們都返回一個(gè)MyDto對(duì)象.
測(cè)試方法如下
[Test]public void ShouldReturn_A_Dto_If_QueryBy_Id_With_Valid_Parameter(){var moq = new Mock<IDataBaseContext<MyDto>>();moq.Setup(a => a.GetElementById(It.IsAny<string>())).Returns(new MyDto());MyBll bll = new MyBll(moq.Object);var dto = bll.GetADto("afakeid");dto.Should().NotBeNull();}這里我們使用到了Moq里的It.Is方法,這個(gè)方法接受一個(gè)Func<T,bool>類型的委托,我們的條件是不管它是一個(gè)什么樣的string,總是返回一個(gè)new MyDto();
[warning]注意這里配置的是Moq對(duì)象(即moq.Object)的方法返回值,而不是bll對(duì)象的方法的返回值,如果我們傳入的字符串是空字符串,則GetADto直接返回了null,數(shù)據(jù)訪問(wèn)對(duì)象就沒機(jī)會(huì)執(zhí)行了.
It里面還有很多靜態(tài)方法,用于指定數(shù)字是否是否在某一范圍,對(duì)象是否是列表中的對(duì)象,字符串是否滿足正則等.語(yǔ)義都非常明確,大家可以自己研究一下.
指定參數(shù)的配置
以上使用到了It.IsAny方法.It里面還有一個(gè)Is方法,接受一個(gè)Func<T,bool>類型委托,用于指定對(duì)象為滿足特定條件的對(duì)象,而不是任意對(duì)象.
Bll層新增以下方法
public bool IsVip(string id){if (string.IsNullOrWhiteSpace(id)) return false;var dto = _dataBaseContext.GetElementById(id);if (dto?.Name?.Contains("sto")) return true;return false;}我們判斷一個(gè)dto是否是vip,如果傳入id為null返回false,如果不是則獲取一個(gè)對(duì)象,如果對(duì)象的名字包含sto關(guān)鍵字則返回true
比如我們知道id為9527的對(duì)象為sto,因此它是個(gè)vip,我們的測(cè)試方法如下
[Test]public void ShouldReturn_True_If_Id_Is_9527(){var moq = new Mock<IDataBaseContext<MyDto>>();moq.Setup(a => a.GetElementById(It.Is<string>(t => t.Trim() == "9527"))).Returns(new MyDto { Name = "sto", Age = 24 });MyBll bll = new MyBll(moq.Object);bool isVip = bll.IsVip("9527");Assert.True(isVip);}以上測(cè)試通過(guò).
MOCk.Of
我們以上僅配置了接口代表的一個(gè)方法,有時(shí)候需要配置多個(gè),這樣需要多個(gè)Setup,這時(shí)候我們可以使用Mock.Of,注意Mock.Of創(chuàng)建出來(lái)的是一個(gè)代理對(duì)象,而不是一個(gè)mock對(duì)象.
[Test]public void MockOf_Test(){var obj = Mock.Of<IDataBaseContext<MyDto>>(a =>a.GetAll()==new List<MyDto>(){new MyDto()}&&a.GetElementById(It.IsAny<string>())==new MyDto()&&a.GetElementsByName(It.IsAny<string>())==new MyDto[3]);var all = obj.GetAll();var one = obj.GetElementById("s");var some = obj.GetElementsByName("somename");Assert.Multiple(() =>{Assert.AreEqual(1, all.Count());Assert.NotNull(one);Assert.AreEqual(3, some.Count());});}以上測(cè)試會(huì)通過(guò).
注意以上的xxx==xxx并不是比較兩個(gè)對(duì)象,Mock利用它進(jìn)行賦值
很多初接觸單元測(cè)試的朋友看完以上代碼后可能感覺一臉懵,完全不理解利用moq在dao層生成一些看似無(wú)意義的假數(shù)據(jù)有什么意義,其實(shí)大家要明白單元測(cè)試的目的是什么,單元測(cè)試是以代碼塊為基礎(chǔ)(通常是一個(gè)方法),測(cè)試這一個(gè)單元邏輯的正確性,在dao層,我們只關(guān)心這一層拿到數(shù)據(jù)后的處理邏輯.很多朋友可能知道ef可以搭建內(nèi)存服務(wù)器來(lái)模擬真實(shí)數(shù)據(jù)庫(kù),這樣也同樣不依賴于外部的數(shù)據(jù)庫(kù).其實(shí)大家也可以這樣做,也可以不這樣而使用moq來(lái)模擬一個(gè)數(shù)據(jù)庫(kù)連接上下文對(duì)象.因?yàn)樵趩卧獪y(cè)試?yán)?真實(shí)的數(shù)據(jù)是什么樣的并不是首要關(guān)心的問(wèn)題,而是這個(gè)代碼單元邏輯的正確性.如果是做集成測(cè)試,我們則需要模擬一個(gè)真實(shí)環(huán)境,這個(gè)時(shí)候我們就需要使用內(nèi)存服務(wù)器甚至使用外部服務(wù)器.當(dāng)然,如果要做壓力測(cè)試,我們還需要模擬產(chǎn)品運(yùn)行時(shí)真實(shí)的物理環(huán)境,網(wǎng)絡(luò)環(huán)境等條件(當(dāng)然,很多時(shí)候直接在真實(shí)的運(yùn)行環(huán)境進(jìn)行測(cè)試了).總之我們要搞清楚不同的測(cè)試要解決什么樣的問(wèn)題,要達(dá)到什么樣的目的,剩下的才是工具框架的使用.
總結(jié)
以上是生活随笔為你收集整理的.net测试篇之Moq框架简单使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 程序员35岁不转型就退休,是真的吗?
- 下一篇: TomatoLog-1.1.0实现ILo