.net測試篇之Moq框架簡單使用

系列目錄html

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方法,咱們在調試模式下看看返回的結果是什麼.

Avatar

它返回了一個空集合,實際上無論咱們提供的是什麼樣的字符串,它都返回一個空集合,這是默認行爲,由於_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來模擬一個數據庫鏈接上下文對象.由於在單元測試裏,真實的數據是什麼樣的並非首要關心的問題,而是這個代碼單元邏輯的正確性.若是是作集成測試,咱們則須要模擬一個真實環境,這個時候咱們就須要使用內存服務器甚至使用外部服務器.固然,若是要作壓力測試,咱們還須要模擬產品運行時真實的物理環境,網絡環境等條件(固然,不少時候直接在真實的運行環境進行測試了).總之咱們要搞清楚不一樣的測試要解決什麼樣的問題,要達到什麼樣的目的,剩下的纔是工具框架的使用.

相關文章
相關標籤/搜索