1. 程式人生 > >轉載:ASP.NET MVC之單元測試分分鐘的事

轉載:ASP.NET MVC之單元測試分分鐘的事

一、為什麼要進行單元測試?
大部分開發者都有個習慣(包括本人在內),常常不喜歡去做單元測試。因為我們對自己寫的程式總是盲目自信,或者存在僥倖心理每次執行通過後就直接扔給測試組的妹子們了。結果妹子一測,大把大把的bug出現了,最後每每看到測試的妹子走過來,心裡就只想說一句話:你是猴子請來的逗比嗎?本來想節省時間,結果最後花在找BUG和修復BUG的這些時間加起來已經比開發這個模組所花的時間還要多了,最後更要命的是,坑爹的加班就在所難免了!如果一開始將bug遏制在萌芽狀態,我們至於這麼苦逼嗎?SO,單元測試很有必要!

二、單元測試法則
1、單元測試必須能夠重複執行,就是能夠非常頻繁地執行

2、單元測試的執行速度不能太慢,要不然會影響開發進度的

3、單元測試不應該依賴於外部資源和真實的環境

4、單元測試不應該涉及到真實資料庫的操作

5、要確保單元測試的可信度

6、單元測試通常以測試一個方法為單位

7、每一個程式猿都需要為自己寫的程式碼編寫單元測試程式碼

三、單元測試工具
我在這裡僅僅推薦一個比較實用的測試工具NUnit,可單獨使用,也可以通過TestDriven.NET(TestDriven.NET是以外掛形式整合在Visual Studio IDE中的單元測試工具,完全相容所有.NET Framework版本,並且集成了多種單元測試框架諸如NUnit,MbUnit,以及 MS Team System 等)將其加入到vs中。

NUnit作為xUnit家族中的.Net成員,是.NET的單元測試框架,xUnit是一套適合於多種語言的單元測試工具。它具有如下特徵:

提供了API,使得我們可以建立一個帶有“通過/失敗”結果的重複單元。
包括了執行測試和表示結果所需的工具。
允許多個測試作為一個組在一個批處理中執行。
非常靈巧,操作簡單,我們花費很少的時間即可學會並且不會給測試的程式新增額外的負擔。
功能可以擴充套件,如果希望更多的功能,可以很容易的擴充套件它。
套用老羅的話就是一句話:它是當今.NET領域最牛逼的測試工具之一

在.NET下的單元測試工具其實非常多,這裡不想多說,我們就使用微軟自己提供的測試框架Unit Test Framework,已經整合在vs中了~

四、MOQ
單元測試的目標是一次只測試一個方法,是一種細粒度的測試,但是假如某個方法依賴於其他一些難以操控的外部東東,比如說網路連線、資料庫連線等時,那麼我們該怎麼辦呢?既然單元測試的法則說不讓依賴這些個外部真實的東西,那還不簡單,我山寨一個不就行了嗎?此時當採用以假亂真的手法來完成單元測試。實際上我們這裡採用的是Mock物件,也就是真實物件的替代品,並使用Moq框架來模擬Mock物件,它為我們提供了模擬真實物件行為的能力,然後交給被測試功能使用,以此判斷被測試功能是否正確。

注意:Moq只能模擬介面或抽象類。

你可以通過Nuget來獲取Moq並且引用到指定的專案,也可以在google上下載,不管怎樣記得在測試專案中引用Moq.dll就行~

舉個例子:

public class Student     {         
         public string ID { get; set; }
         public string Name { get; set; }
         public int Age { get; set; }   
}
IStudentRepository
public interface IStudentRepository  
{        Student GetStudentById(string id); }

下面是方法GetStudentById的單元測試程式碼:
[TestMethod] 
public void GetStudentByIdTest() 
{             //建立MOCK物件
            var mock = new Mock<IStudentRepository>();             //設定MOCK呼叫行為
            mock.Setup(p=>p.GetStudentById("1")).Returns(new Student());             //MOCK呼叫方法
            mock.Object.GetStudentById("1");             Assert.AreNotSame(new Student(), mock.Object.GetStudentById("1")); 
<pre name="code" class="csharp">}
這裡其實已經以假亂真了,因為真實的介面IStudentRepository裡邊的方法GetStudentById在實際中肯定要要訪問資料庫的,那麼我們這裡壓根都麼有訪問什麼資料庫,直接用IStudentRepository介面模擬了一個物件,根本不用實現,這樣瞬間就提高了單元測試的可行性。不過這裡也要提個醒,就是在寫程式碼的時候,別讓程式碼產生過度的依賴,方可在進行單元測試時順利進行!

說說常用的Moq成員:
1、Mock<T>:通過這個類我們能夠得到一個Mock<T>物件,T可以是介面和類。它有一個公開的Object屬性,這個就是我們Moq為我們模擬出的物件。
var mo = new Mock<IStudentRepository>(); mo.Object //其實就是模擬實現IStudentRepository介面的物件
2、It:這是一個靜態類,用於過濾引數。

It很適合用來匹配數字,字串引數,它提供瞭如下幾個靜態方法:

Is<TValue> :引數為Expression<Predict<TValue>>型別,當你需要某種型別並且這種型別要通過程式碼來判斷的話可以使用它。

IsAny<TValue> :沒有引數,只要是TValue型別的就能匹配成功。

IsInRange<TValue> :用來匹配兩個的TValue型別值之間的引數。(Range引數可以設定開閉區間)

IsRegex:用正則表示式匹配。(僅限於字串型別引數)
var customer = new Mock<ICustomer>();
customer.Setup(x => x.SelfMatch(It.Is<int>(i => i % 2 == 0))).Returns("1");//方法SelfMatch接受int型引數,當引數為偶數時,才返回字串1。 customer.Setup(p => p.SelfMatch(It.IsAny<int>())).Returns((int k) => "任何數:" + k);//方法SelfMatch接受int型,且任何int型引數都可以,然後返回:"任何數:" + k。
customer.Setup(p => p.SelfMatch(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns("10以內的數");//方法SelfMatch接受int型,且當範圍在[0,10]時,才返回10以內的數
customer.Setup(p => p.ShowException(It.IsRegex(@"^d+$"))).Throws(new Exception("不能是數字"));//用正則表示式過濾引數不能是數字

3、MockBehavior:用於配置MockObject的行為,比如是否自動mock。

Moq有個列舉型別MockBehavior,有三個值Strict,Loose,Default。

Strict表示Mock物件在呼叫一個方法前這個方法必須被Mock掉,否則就會引發MockException。而Loose與之相反,如果呼叫沒有Mock的方法也不會出錯。Default預設為Loose。例如:
[TestMethod] public void MoqTest()         {             var mo = new Mock<ICustomer>(MockBehavior.Strict);
              mo.Object.Method();//在MockBehavior.Strict設定下,一切呼叫未填充的方法/屬性/事件時會丟擲異常
         }

4、MockFactory:Mock物件工廠,能夠批量生產統一自定義配置的Mock物件,也能批量的進行Mock物件測試。

這是一個模擬物件的工廠,我們不可以成批Mock它們,例如:
var factory = new MockFactory(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };             // Create a mock using the factory settings
            var aMock = factory.Create<IStudent>();             // Create a mock overriding the factory settings
            var bMock = factory.Create<ITeacher>(MockBehavior.Loose);             // Verify all verifiable expectations on all mocks created through the factory
            factory.Verify();

5、Match<T>:如果你先覺得It不夠用就用Match<T>,通過它能夠完全自定義規則。

還是舉個栗子比較能說明問題

[TestMethod()] 
        public void MoqTest()         
        {             
            var mo = new Mock<IRepository>();
            mo.Setup(p => p.Method(MatchHelper.ParamMatcher("wang"))).Returns("success");
            Assert.AreEqual(mo.Object.("wang"), “success);

        } //此處就實現了自定義的引數匹配 
        public static class MatchHelper         
        {             
            public static string ParamMatcher(string name)
            {                 
                return Match<string>.Create(p => p.Equals(name));
            }          
        }


6、Verify和VerifyAll

用於測試mock物件的方法或屬性是否被呼叫執行,Verify必須要先呼叫Verifiable()方法才能用,而VerifyAll不用這樣就可以對所有的mock物件進行驗證,例如:
public void TestVerify()
        {
            var customer = new Mock<ICustomer>(); customer.Setup(p => p.GetCall(It.IsAny<string>()))
              .Returns("方法呼叫").Verifiable();//必須呼叫Verifiable()方法才可以
            customer.Object.GetCall("呼叫了!"); customer.Verify();
        }
        public void TestVerifyAll()
        {
            var customer = new Mock<ICustomer>(); customer.Setup(p => p.GetCall(It.IsAny<string>()))
                                .Returns("方法呼叫"); //沒有顯式呼叫Verifiable()方法也可以
            customer.Object.GetCall("呼叫了!"); customer.VerifyAll();
        }

7、Callback

其實就是回撥,使用Callback可以使我們在某個使用特定引數匹配的方法在被呼叫時得到通知。當執行某方法時,呼叫其內部輸入的(Action)委託,例如:
public void TestCallback()
        {
            var customer = new Mock<ICustomer>(); customer.Setup(p => p.GetCall(It.IsAny<string>()))
                            .Returns("方法呼叫").Callback((string s) => Console.WriteLine("ok" + s)); customer.Object.GetCall("x");
        }

五、ASP.NET MVC單元測試應用
幾點建議:

1、每當你向controller、service、repository層中新增一系列的新函式時,從你開始修改程式碼的那一刻開始,你就必須得承擔有可能破壞原本正常工作的那部分功能的風險。言外之意,你必須進行單元測試才行。

2、單元測試必須是可以快速執行的。因此對於耗時的資料庫互動來說,你必須對其進行mock,然後編寫程式碼與mock的資料庫進行互動

3、你不必為view進行單元測試。因為要想對view進行測試,你就不得不搭建web伺服器。因為搭建web伺服器相對來說很耗時,因此並不推薦針對view進行單元測試。 如果你的view包含大量複雜的邏輯,則你應當考慮將這些邏輯轉移到Helper方法中。你可以針對Helper方法編寫單元測試且無需搭建web伺服器。

4、對於涉及到http的東東,你也必須mock一下

如何為方法新增單元測試?

1、在新建MVC專案時為專案新增預設的單元測試專案,如圖所示:



2、或者在vs中相應的方法處單擊滑鼠右鍵,新增單元測試即可,如圖所示:


MVC單元測試

預設生成的單元測試程式碼已經為Controller生成了相應的單元測試方法,例如對HomeController進行單元測試,注意測試類的命名規範,以及兩個特性TestClass和TestMethod,有了這兩個東東,方可對類和方法進行測試。我們可以發現是按照arrange/act/assert的模式來進行單元測試的,單元測試說白了就是三步走:arrange:初始化測試的環境屬於準備階段;act:執行測試;assert:斷言,測試的結果

        [TestClass]
        public class HomeControllerTest
        {
            [TestMethod]
            public void About()
            {             // Arrange
                HomeController controller = new HomeController();             // Act
                ViewResult result = controller.About() as ViewResult;             // Assert
                Assert.IsNotNull(result);
            }
        }

難點其實在第一步,就是測試環境的準備,這裡更多的是用Moq來進行模擬。另外,涉及到的Assert類主要有以下這些方法

Assert.Inconclusive() 表示一個未驗證的測試;

Assert.AreEqual() 測試指定的值是否相等,如果相等,則測試通過;

AreSame() 用於驗證指定的兩個物件變數是指向相同的物件,否則認為是錯誤

AreNotSame() 用於驗證指定的兩個物件變數是指向不同的物件,否則認為是錯誤

Assert.IsTrue() 測試指定的條件是否為True,如果為True,則測試通過;

Assert.IsFalse() 測試指定的條件是否為False,如果為False,則測試通過;

Assert.IsNull() 測試指定的物件是否為空引用,如果為空,則測試通過;

Assert.IsNotNull() 測試指定的物件是否為非空,如果不為空,則測試通過;

一個模擬訪問Service服務的單元測試栗子:

namespace Mvc4UnitTesting.Tests.Controllers
{
    [TestClass]
    public class HomeControllerTest
    {
        [TestMethod]
        public void Index()
        {             // Arrange
            var mockIProductService = new Mock<IProductService>(); mockIProductService.Setup(p => p.GetAllProduct()).Returns(new List<Product> { new Product { ProductId = 1, ProductName = "APPLE", Price = "5999" } }); HomeController controller = new HomeController(mockIProductService.Object);             // Act
            ViewResult result = controller.Index() as ViewResult; var product = (List<Product>)result.ViewData.Model;             // Assert
            Assert.AreEqual("APPLE", product.First<Product>().ProductName);
        }
    }
}

一個模擬訪問Web環境的單元測試栗子:
public ActionResult Index() 
        { 
            ViewData["Message"] = Request.QueryString["WW"]; return View(); 
        }

        [TestMethod]
        public void Index() 
        { 
            HomeController controller = new HomeController(); 
            var httpContext = new Mock<HttpContextBase>(); 
            var request = new Mock<HttpRequestBase>(); 
            NameValueCollection queryString = new NameValueCollection(); 
            queryString.Add("WW", "WW"); 
            request.Setup(r => r.QueryString).Returns(queryString); 
            httpContext.Setup(ht => ht.Request).Returns(request.Object); 
            ControllerContext controllerContext = new ControllerContext(); 
            controllerContext.HttpContext = httpContext.Object; controller.ControllerContext = controllerContext; 
            ViewResult result = controller.Index() as ViewResult; 
            ViewDataDictionary viewData = result.ViewData; 
            Assert.AreEqual("WW", viewData["Message"]); 
        }

總結:
有效的測試是軟體質量的保證,所以這裡希望大家,包括本人自己在內,都能夠把單元測試落到實處,目前對於我們來說,最大的難點在於能否恰到好處地模擬出相關的依賴資源,因此寫出低耦合的程式碼就變得很有必要。其實多加練習使用之後,自然就能夠應對相對複雜的單元測試,終有一天你會發現,單位測試只不過是分分鐘的事!