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


系列目錄

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來模擬一個數據庫連接上下文對象.因為在單元測試里,真實的數據是什么樣的並不是首要關心的問題,而是這個代碼單元邏輯的正確性.如果是做集成測試,我們則需要模擬一個真實環境,這個時候我們就需要使用內存服務器甚至使用外部服務器.當然,如果要做壓力測試,我們還需要模擬產品運行時真實的物理環境,網絡環境等條件(當然,很多時候直接在真實的運行環境進行測試了).總之我們要搞清楚不同的測試要解決什么樣的問題,要達到什么樣的目的,剩下的才是工具框架的使用.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM