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

1、爲何要進行單元測試?

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

2、單元測試法則

一、單元測試必須可以重複執行,就是可以很是頻繁地執行正則表達式

二、單元測試的執行速度不能太慢,要否則會影響開發進度的數據庫

三、單元測試不該該依賴於外部資源和真實的環境服務器

四、單元測試不該該涉及到真實數據庫的操做網絡

五、要確保單元測試的可信度框架

六、單元測試一般以測試一個方法爲單位函數

七、每個程序猿都須要爲本身寫的代碼編寫單元測試代碼工具

3、單元測試工具

我在這裏僅僅推薦一個比較實用的測試工具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中了~

4、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")); }

這裏其實已經以假亂真了,由於真實的接口IStudentRepository裏邊的方法GetStudentById在實際中確定要要訪問數據庫的,那麼咱們這裏壓根都麼有訪問什麼數據庫,直接用IStudentRepository接口模擬了一個對象,根本不用實現,這樣瞬間就提升了單元測試的可行性。不過這裏也要提個醒,就是在寫代碼的時候,別讓代碼產生過分的依賴,方可在進行單元測試時順利進行!

說說經常使用的Moq成員

一、Mock<T>:經過這個類咱們可以獲得一個Mock<T>對象,T能夠是接口和類。它有一個公開的Object屬性,這個就是咱們Moq爲咱們模擬出的對象。

var mo = new Mock<IStudentRepository>();
mo.Object //其實就是模擬實現IStudentRepository接口的對象

二、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("不能是數字"));//用正則表達式過濾參數不能是數字

 三、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設置下,一切調用未填充的方法/屬性/事件時會拋出異常
         }

四、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();

五、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)); } }

 六、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(); }

七、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");
}

5、ASP.NET MVC單元測試應用

幾點建議

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

二、單元測試必須是能夠快速執行的。所以對於耗時的數據庫交互來講,你必須對其進行mock,而後編寫代碼與mock的數據庫進行交互

三、你沒必要爲view進行單元測試。由於要想對view進行測試,你就不得不搭建web服務器。由於搭建web服務器相對來講很耗時,所以並不推薦針對view進行單元測試。 若是你的view包含大量複雜的邏輯,則你應當考慮將這些邏輯轉移到Helper方法中。你能夠針對Helper方法編寫單元測試且無需搭建web服務器。

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

如何爲方法添加單元測試?

一、在新建MVC項目時爲項目添加默認的單元測試項目,如圖所示:

二、或者在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"]);
        }

總結

有效的測試是軟件質量的保證,因此這裏但願你們,包括本人本身在內,都可以把單元測試落到實處,目前對於咱們來講,最大的難點在於可否恰到好處地模擬出相關的依賴資源,所以寫出低耦合的代碼就變得頗有必要。其實多加練習使用以後,天然就可以應對相對複雜的單元測試,終有一天你會發現,單位測試只不過是分分鐘的事!

相關文章
相關標籤/搜索