系列目錄
Moq庫簡介及安裝
Moq簡介
Moq是.net平台下的一個非常流行的模拟庫,隻要有一個接口它就可以動态生成一個對象,底層使用的是Castle的動态代理功能.
它的流行賴于依賴注入模式的興起,現在越來越多的分層架構使用依賴注入的方式來解耦層與層之間的關系.最為常見的是資料層和業務邏輯層之間的依賴注入,業務邏輯層不再強依賴資料層對象,而是依賴資料層對象的接口,在IOC容器裡完成依賴的配置.
這種解耦給單元測試帶來了巨大的便利,使得對業務邏輯的測試可以脫離對資料層的依賴,單元測試的粒度更小,更容易排查出問題所在.
大家可能都知道,資料層的接口往往有很多方法,少則十幾個,多則幾十個.我們如果在單元測試的時候把接口切換為假實作,即使實作類全是空也需要大量代碼,并且這些代碼不可重用,一旦接口層改變不但要更改真實資料層實作還要修改這些專為測試做的假實作.這顯然是不小的工作量.
幸好有Moq,它可以在編譯時動态生成接口的代理對象.大大提高了代碼的可維護性,同時也極大減少工作量.
除了動态建立代理外,Moq還可以進行行為測試,觸發事件等.
Moq安裝
Moq安裝非常簡單,在Nuget裡面搜尋moq,第一個結果便是moq架構,點選安裝即可.
Moq簡單使用
本示例中要使用到的代碼如下
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為業務層和資料層互動的對象,IDataBaseContext為資料層接口,MyBll為我們的業務邏輯層
我們要測試的是業務邏輯層的代碼.這裡業務邏輯類并沒有無參構造函數,如果手動建立起來非常麻煩,裡面的坑前面說過.下面看如何使用Moq來模拟一個IDataBaseContext對象
我們編寫以下測試類
[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如果傳的參數是null或者空就會傳回一個null對象,因些傳回的結果是Null,以上測試會通過.
這裡我們首先建立了一個moq對象,它的Object屬性就是我們要模拟的IDataBaseContext對象,我們在建立MyBll對象時把它作為參數傳入.
Moq基本配置
我們再為MyBll添加以下方法
public IEnumerable<MyDto> GetDtos(string name)
{
if (string.IsNullOrWhiteSpace(name)) return null;
var dtos = _dataBaseContext.GetElementsByName(name);
return dtos;
}
我們編寫如下測試方法
[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");
}
以上測試方法調用了bll的GetDtos方法,我們知道GetDtos内部調用了資料通路接口的GetElementsByName方法,我們在調試模式下看看傳回的結果是什麼.
它傳回了一個空集合,實際上不管我們提供的是什麼樣的字元串,它都傳回一個空集合,這是預設行為,因為
_dataBaseContext.GetElementsByName
并不知道我們的真實邏輯是什麼.
這樣很顯然并不是總能滿足我們的要求,很多時候我們在測試業務邏輯層的時候需要具體的資料,然後才能繼續往下走.
比如以下方法,我們擷取資料庫裡的所有資料,然而通過一系列邏輯進行過濾,最終傳回過濾後的結果.
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;
}
如果是預設行為(調用模拟的接口方法,引用對象傳回null,集合傳回空,簡單對象傳回預設值),則代碼很快就傳回了,if下面的業務邏輯測不到了.下面我們看下如何配置接口方法的傳回值
這裡其實主要用到了 建立moq對象的
setup
方法,我們可以在setup裡設定方法,屬性的值.
[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();
}
我們看以上代碼,我們我們讓資料通路接口的代理對象傳回一個MyDto類型集合,一共四個元素,由我們的業務可知,我們隻要年齡大于20的元素,并且名字按正序排列.是以以上測試應該傳回成功,實際上也是測試通過了.
帶參數的方法設定
以上的GetAll是不帶參數的,帶參數的方法我們可以顯式的指定一個參數,我們也可以使用Moq架構提供的方法來模糊指定參數,比如我們可以指定方法是任意字元,任意數字,任意範圍的數字等.
我們再看前面的一個方法
public MyDto GetADto(string id)
{
if (string.IsNullOrWhiteSpace(id)) return null;
return _dataBaseContext.GetElementById(id);
}
這個方法接收一個類型為字元串的id,隻要字元串不是空字元串或者null時我們都傳回一個MyDto對象.
測試方法如下
[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方法,這個方法接受一個Func<T,bool>類型的委托,我們的條件是不管它是一個什麼樣的string,總是傳回一個new MyDto();
[warning]注意這裡配置的是Moq對象(即moq.Object)的方法傳回值,而不是bll對象的方法的傳回值,如果我們傳入的字元串是空字元串,則GetADto直接傳回了null,資料通路對象就沒機會執行了.
It裡面還有很多靜态方法,用于指定數字是否是否在某一範圍,對象是否是清單中的對象,字元串是否滿足正則等.語義都非常明确,大家可以自己研究一下.
指定參數的配置
以上使用到了It.IsAny方法.It裡面還有一個Is方法,接受一個Func<T,bool>類型委托,用于指定對象為滿足特定條件的對象,而不是任意對象.
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;
}
我們判斷一個dto是否是vip,如果傳入id為null傳回false,如果不是則擷取一個對象,如果對象的名字包含sto關鍵字則傳回true
比如我們知道id為9527的對象為sto,是以它是個vip,我們的測試方法如下
[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);
}
以上測試通過.
MOCk.Of
我們以上僅配置了接口代表的一個方法,有時候需要配置多個,這樣需要多個Setup,這時候我們可以使用Mock.Of,注意Mock.Of建立出來的是一個代理對象,而不是一個mock對象.
[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());
});
}
以上測試會通過.
注意以上的xxx==xxx并不是比較兩個對象,Mock利用它進行指派
很多初接觸單元測試的朋友看完以上代碼後可能感覺一臉懵,完全不了解利用moq在dao層生成一些看似無意義的假資料有什麼意義,其實大家要明白單元測試的目的是什麼,單元測試是以代碼塊為基礎(通常是一個方法),測試這一個單元邏輯的正确性,在dao層,我們隻關心這一層拿到資料後的處理邏輯.很多朋友可能知道ef可以搭建記憶體伺服器來模拟真實資料庫,這樣也同樣不依賴于外部的資料庫.其實大家也可以這樣做,也可以不這樣而使用moq來模拟一個資料庫連接配接上下文對象.因為在單元測試裡,真實的資料是什麼樣的并不是首要關心的問題,而是這個代碼單元邏輯的正确性.如果是做內建測試,我們則需要模拟一個真實環境,這個時候我們就需要使用記憶體伺服器甚至使用外部伺服器.當然,如果要做壓力測試,我們還需要模拟産品運作時真實的實體環境,網絡環境等條件(當然,很多時候直接在真實的運作環境進行測試了).總之我們要搞清楚不同的測試要解決什麼樣的問題,要達到什麼樣的目的,剩下的才是工具架構的使用.