天天看點

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

這裡其實已經以假亂真了,因為真實的接口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項目時為項目添加預設的單元測試項目,如圖所示:

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

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

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

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

總結

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

鄭重聲明:本部落格僅用于個人整理和總結學習筆記,如有任何疑問歡迎留言!

繼續閱讀