一、為什么要進行單元測試?
大部分開發者都有個習慣(包括本人在內),常常不喜歡去做單元測試。因為我們對自己寫的程序總是盲目自信,或者存在僥幸心理每次運行通過后就直接扔給測試組的妹子們了。結果妹子一測,大把大把的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項目時為項目添加默認的單元測試項目,如圖所示:
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"]); }
總結
有效的測試是軟件質量的保證,所以這里希望大家,包括本人自己在內,都能夠把單元測試落到實處,目前對於我們來說,最大的難點在於能否恰到好處地模擬出相關的依賴資源,因此寫出低耦合的代碼就變得很有必要。其實多加練習使用之后,自然就能夠應對相對復雜的單元測試,終有一天你會發現,單位測試只不過是分分鍾的事!