MVC 基本工具(Visual Studio 的單元測試、使用Moq)

來源:互聯網
上載者:User

標籤:興趣   介面   unit   改善   ram   建立   集中   variable   註解   

3.Visual Studio 的單元測試

有很多.NET單元測試包,其中很多是開源和免費的。本文打算使用 Visual Studio 附帶的內建單元測試支援,但其他一些.NET單元測試包也是可用的。

為了示範Visual Studio的單元測試支援,本例打算對樣本項目添加一個 IDiscountHelper 介面的新實現。 在 Models 檔案夾下建立類檔案 MinimumDiscountHelper.cs :

namespace EssentiaTools.Models{    public class MinimumDiscountHelper:IDiscountHelper    {        public decimal ApplyDiscount(decimal totalParam)        {            throw new NotImplementedException();        }    }}

此例的的目標是讓 MinimumDiscountHelper 示範以下行為:

· 總額大於 $100時,折扣為10%

· 總額介於(並包括)$10~$100之間時,折扣為$5

· 總額小於$10時,無折扣

· 總額為負值時,拋出 ArgumentOutOfRangeException

 

3.1 建立單元測試項目

承接  【MVC 4】3.MVC 基本工具(建立樣本項目、使用 Ninject) 的項目“EssentiaTools”,右擊方案總管中的頂級條目,從彈出的菜單中選擇“Add New Project(建立項目)”

 

在彈出的對話方塊中,添加“Unit Test Project(單元測試項目)”,將項目名設定為EssentiaTools.Tests

 

然後對這一測試專案添加一個引用,以便能夠對MVC 項目中的類執行測試。

 

3.2 建立單元測試

在 Essential.Tests 項目的 UnitTest1.cs 檔案中添加單元測試:

using System;using Microsoft.VisualStudio.TestTools.UnitTesting;using EssentiaTools.Models;namespace EssentiaTools.Tests{    [TestClass]    public class UnitTest1    {        private IDiscountHelper getTestObject()        {            return new MinimumDiscountHelper();        }        [TestMethod]        public void Discount_Above_100()        {            //準備            IDiscountHelper target = getTestObject();            decimal total = 200;            //動作            var discountedTotal = target.ApplyDiscount(total);            //斷言            Assert.AreEqual(total * 0.9M, discountedTotal);        }    }}

只添加了一個單元測試。含有測試的類是用 TestClass 註解屬性進行注釋的,其中的各個測試都是用 TestMethod 註解屬性進行注釋方法。並不是單元測試類中的所有方法都是單元測試。例如 getTestObject 方法因為該方法沒有 TestMethod 註解屬性,故 Visual Studio 不會把它當作一個單元測試。

可以看出,單元測試方法遵循了“準備/動作/斷言(A/A/A)”模式。

上述測試方法是通過調用 getTestObject 方法建立起來的,getTestObject 方法建立了一個待測試的執行個體 —— 本例為 MinimumDiscountHelper 類。另外還定義了要進行檢查的 total 值,這是單元測試的“準備(Arrange)” 部分。

對於測試的“動作(Act)”部分,調用 MinimumDiscountHelper.AppleDiscount 方法,並將結果賦給 discountedTotal 變數。最後,對於測試的“斷言(Assert)”部分使用了 Assert.AreEqual 方法,以檢查從 AppleDiscount 方法得到的值是最初總額的90% 。

 

Assert 類有一系列可以在測試中使用的靜態方法。這個類位於 Microsoft.VisualStudio.TestTools.UnitTesting 命名空間,該命名空間還包含了一些對建立和執行測試有用的其他類。有關該命名空間的類,可以參閱:https://msdn.microsoft.com/en-us/library/ms182530.aspx

Assert 類是用的最多的一個,其中重要的一些方法如下:

Assert 類中的每一個靜態方法都可以檢查單元測試的某個方面。如果宣告失敗,將拋出一個異常,這意味著整個單元測試失敗。由於每一個單元測試都是獨立進行處理的,因此其他單元測試將被繼續執行。

上述的每一個方法都有一個string 為參數的重載,該字串作為宣告失敗時的訊息元素。 AreEqual 和 AreNotEqual 方法有幾個重載,以滿足特定類型的比較。例如,有一個版本可以比較字串, 而不需要考慮其他情況。

提示:Microsoft.VisualStudio.TestTools.UnitTesting 命名空間中一個值得注意的成員是 ExpectedException 屬性。這是一個斷言,只有當單元測試拋出 ExceptionType 參數指定類型的異常時,該斷言才是成功的。這是一種確保單元測試拋出異常的整潔方式,而不需要在單元測試中構造 try..catch 塊

 

為了驗證前述 MinimumDiscountHelper 的其他行為,修改檔案 UnitTest1.cs 如下:

using System;using Microsoft.VisualStudio.TestTools.UnitTesting;using EssentiaTools.Models;namespace EssentiaTools.Tests{    [TestClass]    public class UnitTest1    {        private IDiscountHelper getTestObject()        {            return new MinimumDiscountHelper();        }        [TestMethod]        public void Discount_Above_100()        {            //準備            IDiscountHelper target = getTestObject();            decimal total = 200;            //動作            var discountedTotal = target.ApplyDiscount(total);            //斷言            Assert.AreEqual(total * 0.9M, discountedTotal);        }        [TestMethod]        public void Discount_Between_10_And_100()        {            //準備            IDiscountHelper target = getTestObject();            //動作            decimal TenDollarDiscount = target.ApplyDiscount(10);            decimal HundredDollarDiscount = target.ApplyDiscount(100);            decimal FiftyDollarDiscount = target.ApplyDiscount(50);            //斷言            Assert.AreEqual(5, TenDollarDiscount, "$10 discount is wrong");            Assert.AreEqual(95, HundredDollarDiscount, "$100 discoutn is wrong");            Assert.AreEqual(45, FiftyDollarDiscount, "$50 discount is wrong");        }        [TestMethod]        public void Discount_Less_Than_10()        {            IDiscountHelper target = getTestObject();            decimal discount5 = target.ApplyDiscount(5);            decimal discount0 = target.ApplyDiscount(0);            Assert.AreEqual(5, discount5);            Assert.AreEqual(0, discount0);        }        [TestMethod]        [ExpectedException(typeof(ArgumentOutOfRangeException))]        public void Discount_Negative_Total()        {            IDiscountHelper target = getTestObject();            target.ApplyDiscount(-1);        }    }}

 

3.3 運行單元測試(並失敗)

Visual Studio 2012 為管理和運行測試引入了一個更為有用的“Test Explorer(測試資源管理員)”視窗。從 Visual Studio 的“Test(測試)”菜單中選擇“Window(視窗)”—>"Test Explorer(測試資源管理員)",便可以看到這一新視窗,點擊左上方附近的“RunAll(全部運行)”按鈕,會看到效果:

可以在該視窗的左側面板中看到所定義的測試清單。所有的測試都失敗了,這是當然的,因為所測試的這些方法還未實現。可以點其中任意測試,測試失敗的原因和細節會顯示在視窗的右側面板中。

 

3.4 實現特性

現在,到了實現特性的時候了。當編碼工作完成時,基本上可以確信代碼是能夠按預期工作的。有了之前的準備,MinimumDiscountHelper 類的實現相當簡單:

using System;using System.Collections.Generic;using System.Linq;using System.Web;namespace EssentiaTools.Models{    public class MinimumDiscountHelper : IDiscountHelper    {        public decimal ApplyDiscount(decimal totalParam)        {            if (totalParam < 0)            {                throw new ArgumentOutOfRangeException();            }            else if (totalParam > 100)            {                return totalParam * 0.9M;            }            else if (totalParam > 10 && totalParam <= 100)            {                return totalParam - 5;            }            else            {                return totalParam;            }        }    }}

 

3.5 測試並修正代碼

為了示範如何利用 Visual Studio 進行單元測試迭代,上述代碼故意留下了一個錯誤。如果點擊“測試資源管理員”視窗中的“全部運行”按鈕,則可以看到該錯誤的效果。測試結果如下:

可以看到,三個單元測試得到了通過,但 Discount_Between_10_And_100 測試方法檢測到了一個問題。當點擊這一失敗的測試時,可以看到測試期望得到的是5,但實際得到的是10。

此刻,重新審視代碼便會發現,並未得到適當的實現——特別是總額是10或100的折扣,未做適當處理。問題出在 MinimumDiscountHelper 類的這句語句上:

...else if (totalParam > 10 && totalParam <= 100)...

雖然目標是建立介於(包括)$10~$100 直接的行為,但實際卻排除了等於$10 的情況,修改成:

...else if (totalParam >= 10 && totalParam <= 100)...

重新運行測試,所有測試代碼都已通過:

 

4. 使用 Moq

前面的單元測試如此簡單的原因之一是因為測試的是一個不依賴於其他類而起作用的單一的類。當然,實際項目中有這樣的類,但往往還需要測試一些不能孤立啟動並執行對象。在這些情況下,需要將注意力於感興趣的類或方法上,才能不必對依賴類也進行隱式測試。

一個有用的辦法是使用模仿對象,它能夠以一種特殊而受控的的方式,來類比項目中實際對象的功能。模仿對象能夠縮小測試的側重點,以使使用者只檢查感興趣的功能。

 

4.1 理解問題

在開始使用 Moq 之前,本例想示範一個試圖要修正的問題。下面打算對 LinqValueCalculator 類進行單元測試,LinqValueCalculator 在前面出現過,具體代碼為:

using System;using System.Collections.Generic;using System.Linq;using System.Web;namespace EssentiaTools.Models{    public class LinqValueCalculator : IValueCalculator    {        private IDiscountHelper discounter;        public LinqValueCalculator(IDiscountHelper discountParam)        {            discounter = discountParam;        }        public decimal ValueProducts(IEnumerable<Product> products)        {            return discounter.ApplyDiscount(products.Sum(p => p.Price));        }    }}

為了,測試這個類,在單元測試項目中新增單元測試檔案 UnitTest2.cs :

using System;using Microsoft.VisualStudio.TestTools.UnitTesting;using EssentiaTools.Models;using System.Linq;namespace EssentiaTools.Tests{    [TestClass]    public class UnitTest2    {        private Product[] products = {                                         new Product{Name="Kayak",Catogory="Watersports",Price=275M},                                         new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M},                                         new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M},                                         new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M}                                     };        [TestMethod]        public void Sum_Products_Correctly()        {            //準備            var discounter = new MinimumDiscountHelper();            var target = new LinqValueCalculator(discounter);            var goalTotal = products.Sum(e => e.Price);            //動作            var result = target.ValueProducts(products);            //斷言            Assert.AreEqual(goalTotal, result);        }    }}

現在面臨的問題是,LinqValueCalculator 類依賴於 IDiscountHelper 介面的實現才能進行操作。此例使用了 MinimumDiscountHelper 類(這是 IDiscountHelper 介面的實作類別),它表現了兩個不同的問題。

第一個問題是單元測試變得複雜和脆弱。為了建立一個能夠進行工作的單元測試,需要考慮 IDiscountHelper 實現中的折扣邏輯,以便判斷出 ValueProducts 方法的預期值。脆弱來自這樣一個事實:一旦該實現中的折扣邏輯發生變化,測試便會失敗。

第二個也是最令人擔憂的問題是已經延展了這一單元測試的範圍,使它的隱式的包含了 MinimumDiscountHelper 類。當單元測試失敗時,使用者不知道問題是出在 LinqValueCalculator 類中,還是在 MinimumDiscountHelper 類中。

當單元測試簡單且焦點集中時,會工作的很好,而當前的設定會讓這兩個特徵都不能得到滿足。而在MVC項目中添加並運用 Moq ,能夠避免這些問題。

 

4.2 將 Moq 添加到VisualStudio 項目

和前面的 Ninject 一樣,在測試專案中 搜尋並添加 NuGet 程式包 Moq 。

 

4.3 對單元測試添加模仿對象

對單元測試添加模仿對象,其目的是告訴 Moq,使用者想使用哪一種對象。對它的行為進行配置,然後將該對象運用於測試目的。

在單元測試中使用 Mock 對象,為 LinqValueCalculator 的單元測試添加模仿對象,修改 UnitTest2.cs 檔案:

using System;using Microsoft.VisualStudio.TestTools.UnitTesting;using EssentiaTools.Models;using System.Linq;using Moq;namespace EssentiaTools.Tests{    [TestClass]    public class UnitTest2    {        private Product[] products = {                                         new Product{Name="Kayak",Catogory="Watersports",Price=275M},                                         new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M},                                         new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M},                                         new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M}                                     };        [TestMethod]        public void Sum_Products_Correctly()        {            //準備            Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();            mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);            var target = new LinqValueCalculator(mock.Object);            //動作            var result = target.ValueProducts(products);            //斷言            Assert.AreEqual(products.Sum(e => e.Price), result);        }    }}

 

第一次使用 Moq 時,可能會覺得其文法有點奇怪,下面將示範該過程的每個步驟。

(1) 建立模仿對象

第一步是要告訴 Moq,使用者想使用的是哪種模仿對象。 Moq 十分依賴於泛型的型別參數,從以下語句可以看到這種參數的使用方式,這是告訴 Moq,要模仿的對象時 IDiscountHelper 實現。

...Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();...

建立一個強型別的的 Mock<IDiscountHelper> 對象,目的是告訴 Moq 庫,它要處理的是哪種類型——當然,這便是用於該單元測試的 IDiscountHelper 介面。單為了改善單元測試的側重點,這可以是想要隔離出來的任何類型。

 

(2) 選擇方法

除了建立強型別的Mock對象外,還需要指定它的行為方式——這是模仿過程的核心,它可以建立模仿所需要的基準行為,使用者可以將這種行為用於對單元測試中目標對象的功能進行測試。以下是單元測試中的語句,它為模仿對象建立了使用者所希望的行為。

... mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);...

用 Setup 方法給模仿對象添加一個方法。 Moq 使用 LINQ 和 lambda 運算式進行工作。在調用 Setup 方法時,Moq 會傳遞要求它的介面。它巧妙地封裝了一些本書不打算細說的LINQ 魔力,這種魔力讓使用者可以選擇想要通過 lambda 運算式進行配置或檢查的方法。對於該單元測試,希望定義 AppleDiscount 方法的行為,它是 IDiscountHelper 介面的唯一方法,也是對 LinqValueCalculator 類進行測試所需要的方法。

必須告訴 Moq 使用者感興趣的參數值是什麼,這是要用 It 類要做的事情,如以下加粗部分所示。

... mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);...

這個It 類定義了許多以泛型型別參數進行使用的方法。此例用 decimal 作為泛型型別調用了 IsAny 方式。這是告訴 Moq ,當以任何十進位為參數來調用 ApplyDiscount 方法時,它應該運用我們定義的這一行為。
下面給出了 It 類所提供的方法,所有的這些方法都是靜態。

 

 

(3) 定義結果

Returns 方法讓使用者指定在調用模仿方法時要返回的結果。其型別參數用以指定結果的類型,而用 lambda 運算式來指定結果。如下:

...
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
...

通過調用帶有 decimal 型別參數的 Returns 方法(即 Returns<decimal>),這是告訴 Moq 要返回一個十進位的值。對於 lambda 運算式,Moq 傳遞了一個在ApplyDiscount 方法中接收的類型值 —— 此例建立了一個穿透方法,該方法返回了傳遞給模仿的 ApplyDiscount 方法的值,並未對這個值執行任何操作。

上述過程的思想是:

為了對 LinqValueCalculator 進行單元測試,如果建立一個 IDiscountHelper 模仿對象,便可以在單元測試中排除 IDiscountHelper 介面的實作類別 MinimumDiscountHelper ,從而使單元測試更為簡單容易。用 Moq 建立模仿對象的整個過程包括了以下幾個步驟:a. 用 Mock 建立模仿對象; b. 用Setup 方法建立模仿對象的行為; c. 用 It 類設定行為的參數; d. 用Return 方法指定行為的傳回型別; e. 用 lambda 運算式在Return 方法中建立具體行為。

 

(4) 使用模仿對象

最後一個步驟是在單元測試中使用這個模仿對象,通過讀取 Mock<IDiscountHelper> 對象的Object 屬性值來實現

...var target = new LinqValueCalculator(mock.Object);...

 

總結下,在上述樣本中,Object 屬性返回 IDiscountHelper 介面的實現,該實現中的 ApplyDiscount 方法返回它傳遞的十進位參數的值。

這使單元測試很容易執行,因為使用者可以自行求取 Product 對象的價格總和,並檢查 LinqValueCalculator 對象得到了相同的值。

...
Assert.AreEqual(products.Sum(e => e.Price), result);...

以這種方式使用 Moq 的好處是,單元測試只檢查 LinqValueCalculator 對象的行為,並不依賴任何 Models 檔案夾中 IDiscountHelper 介面的真實實現。這意味著當測試失敗時,使用者便知道問題出在 LinqValueCalculator 實現中,或建立模仿對象的方式中。而解決源自這些方面的問題,比處理實際對象鏈及其相互互動,要更叫簡單而容易。

 

4.4 建立更複雜的模仿對象

前面展示了一個十分簡單的模仿對象,但 Moq 最漂亮的部分是快速建立複雜行為以便對不同情況進行測試的能力。在 UnitTest2.cs  中建立一個單元測試,模仿更加複雜的 IDiscountHelper 介面實現。

using System;using Microsoft.VisualStudio.TestTools.UnitTesting;using EssentiaTools.Models;using System.Linq;using Moq;namespace EssentiaTools.Tests{    [TestClass]    public class UnitTest2    {        private Product[] products = {                                         new Product{Name="Kayak",Catogory="Watersports",Price=275M},                                         new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M},                                         new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M},                                         new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M}                                     };        [TestMethod]        public void Sum_Products_Correctly()        {            //準備            Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();            mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);            var target = new LinqValueCalculator(mock.Object);            //動作            var result = target.ValueProducts(products);            //斷言            Assert.AreEqual(products.Sum(e => e.Price), result);        }        private Product[] createProduct(decimal value)        {            return new[] { new Product { Price = value } };        }        [TestMethod]        [ExpectedException(typeof(System.ArgumentOutOfRangeException))]        public void Pass_Through_Variable_Discounts()        {            Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();            mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);            mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0))).Throws<System.ArgumentOutOfRangeException>();            mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => (total * 0.9M));            mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive))).Returns<decimal>(total => total - 5);            var target = new LinqValueCalculator(mock.Object);            decimal FiveDollarDiscount = target.ValueProducts(createProduct(5));            decimal TenDollarDiscount = target.ValueProducts(createProduct(10));            decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50));            decimal HundredDollarDiscount = target.ValueProducts(createProduct(100));            decimal FiveHundredDollarDiscount = target.ValueProducts(createProduct(500));            Assert.AreEqual(5, FiveDollarDiscount, "$5 Fail");            Assert.AreEqual(5, TenDollarDiscount, "$10 Fail");            Assert.AreEqual(45, FiftyDollarDiscount, "$50 Fail");            Assert.AreEqual(95, HundredDollarDiscount, "$100 Fail");            Assert.AreEqual(450, FiveHundredDollarDiscount, "$500 Fail");            target.ValueProducts(createProduct(0));        }    }}

在單元測試期間,複製另一個模型類期望的行為似乎是在做一個奇怪的事情,但這能夠完美示範 Moq 的一些不同用法。

可以看出,根據所接收到的參數值,定義了 ApplyDiscount 方法的四個不同的行為。最簡單的行為是“全匹配”,它直接返回任意的decimal 值,如下:

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

這是用於上一樣本的同一行為,把它放在這是因為調用 Setup 方法的順序會影響模仿對象的行為。Moq 會以相反的順序評估所給定的行為,因此會考慮調用最後一個 Setup 方法。這意味著,使用者必須按從最一般到最特殊的順序,小心地建立模仿行為。 It.IsAny<decimal> 是此例所定義的最一般的條件,因而首先運用它。如果顛倒調用 Setup 的順序,該行為將能匹配對 ApplyDiscount 方法的所有調用,並建置錯誤的模仿結果。

 

(1) 模仿特定值(並拋出異常)

對於 Setup 方法第二個調用,使用了 It.Is 方法

 mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0))).Throws<System.ArgumentOutOfRangeException>();

若傳遞給 ApplyDiscount 方法的值是0,則 Is方法的謂詞便返回 true。這裡並未返回一個結果,而是使用了 Throws 方法,這會讓 Moq 拋出一個用型別參數指定的異常執行個體。

樣本還用 Is 方法捕捉了大於100的值:

mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => (total * 0.9M));

Is.It 方法是為不同參數值建立指定行為最靈活的方式,因為使用者可以使用任意謂詞來返回 true 或 false 。在建立複雜模仿對象的,這是最常用的方法。

 

(2) 模仿值的範圍

It 對象最後是和 IsInRange 方法一起使用的,它讓使用者能夠捕捉參數值的範圍。

mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive))).Returns<decimal>(total => total - 5);

這裡介紹這一方法是出於完整性,如果是在使用者自己的項目,可以使用 It 方法和一個謂詞來做同樣的事情,如下所示:

mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v=>v>=10&&v<=100))).Returns<decimal>(total => total - 5);

效果是相同的,但謂詞方法更為靈活。Moq 有一系列非常有用的特性,閱讀https://github.com/Moq/moq4/wiki/Quickstart上提供的入門指南,可以看到許多用法。

 

源碼地址:https://github.com/YeXiaoChao/EssentiaTools

 

引用 http://www.cnblogs.com/yc-755909659/p/5254427.html

MVC 基本工具(Visual Studio 的單元測試、使用Moq)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.