溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

.NET重構(gòu)—單元測(cè)試重構(gòu)

發(fā)布時(shí)間:2020-08-07 09:03:04 來(lái)源:網(wǎng)絡(luò) 閱讀:1232 作者:王清培 欄目:編程語(yǔ)言

閱讀目錄:

  • 1.開(kāi)篇介紹

  • 2.單元測(cè)試、測(cè)試用例代碼重復(fù)問(wèn)題(大量使用重復(fù)的Mock對(duì)象及測(cè)試數(shù)據(jù))

    • 2.1.單元測(cè)試的繼承體系(利用超類(lèi)來(lái)減少M(fèi)ock對(duì)象的使用)

      • 2.1.1.公用的MOCK對(duì)象;

      • 2.1.2.公用的MOCK行為;

      • 2.1.3.公用的MOCK數(shù)據(jù);



  • 3.LINQ表達(dá)式的重構(gòu)寫(xiě)法(將必要的LINQ寫(xiě)成普通的Function穿插在LINQ表達(dá)式中)

  • 4.面向特定領(lǐng)域的單元測(cè)試框架(一切原則即是領(lǐng)域驅(qū)動(dòng))

    • 4.1.分散測(cè)試邏輯、日志記錄(讓測(cè)試邏輯可以重組,記錄形式為領(lǐng)域模型)

    • 4.2.測(cè)試用例的數(shù)據(jù)重用(為自動(dòng)化測(cè)試準(zhǔn)備固定數(shù)據(jù),建立Assert的比較測(cè)試數(shù)據(jù))


1】開(kāi)篇介紹

最近一段時(shí)間結(jié)束了一個(gè)Sprint,在這次的開(kāi)發(fā)當(dāng)中有些東西覺(jué)得還不錯(cuò)有總結(jié)分享的價(jià)值,所以整理成本文;

重構(gòu)已是老生常談的話題,我們或多或少對(duì)它有所了解但是對(duì)它的深刻理解恐怕需要一段實(shí)踐過(guò)后才能體會(huì)到;提到重構(gòu)就不得不提為它保駕護(hù)航的大功臣單元測(cè)試,重構(gòu)能有今天的風(fēng)光影響力完全少不了單元測(cè)試的功勞;最近一段時(shí)間寫(xiě)單元測(cè)試用例的時(shí)間遠(yuǎn)超過(guò)我寫(xiě)邏輯代碼的時(shí)間和多的多的代碼量,這是為什么?我一開(kāi)始很難給自己一個(gè)理由去做好這件事,心態(tài)上還是轉(zhuǎn)變不過(guò)來(lái),可是每當(dāng)我心浮氣躁的時(shí)候它總能給我點(diǎn)驚喜,讓我繼續(xù)下去,天生具有好奇心的程序員怎么會(huì)就此結(jié)束呢,只有到達(dá)了一扇門(mén)之后我們回過(guò)頭來(lái)看一下走的路才能真正的明白這是條對(duì)的路還是錯(cuò)的路;

單元測(cè)試簡(jiǎn)單寫(xiě)起來(lái)沒(méi)有什么太大問(wèn)題,但是我們不僅為了達(dá)到代碼的100%覆蓋還要到達(dá)到邏輯的100%覆蓋,代碼的覆蓋不代表邏輯的覆蓋;一個(gè)簡(jiǎn)單的邏輯判斷雖然只有一行代碼,但是里面可能會(huì)有正反向很多種邏輯在里面;比如:Order.ToString()簡(jiǎn)單的代碼,想要覆蓋很簡(jiǎn)單,只要對(duì)象不為空都能正確的覆蓋到,但是如果我們沒(méi)有測(cè)試到它為NULL的情況下的邊界邏輯,這個(gè)時(shí)候我們就會(huì)漏掉這種可能會(huì)導(dǎo)致BUG的邏輯路徑;所以我們會(huì)盡可能的多去寫(xiě)用例來(lái)達(dá)到最終的理想效果;

(總之把單元測(cè)試的所有精力集中在可能會(huì)出問(wèn)題的地方,也是自己最擔(dān)心的地方,這個(gè)地方通常是邏輯比較復(fù)雜的地方;)

2】單元測(cè)試、測(cè)試用例代碼重復(fù)問(wèn)題(大量使用重復(fù)的Mock對(duì)象及測(cè)試數(shù)據(jù))

單元測(cè)試代碼中最常見(jiàn)的代碼就是Mock或者Fake接口邏輯,那么在一個(gè)具有上百個(gè)用例覆蓋的代碼中會(huì)同時(shí)使用到一組相關(guān)的Mock接口對(duì)象,這無(wú)形中增加了我們編寫(xiě)單元測(cè)試的效率給后期的維護(hù)測(cè)試用例帶來(lái)了很大的隱患及工作量;

單元測(cè)試代碼的組成都是按照用例來(lái)劃分,一個(gè)用例可以用來(lái)包括一個(gè)單一入口的所有邏輯也可以是一個(gè)判斷分支的部分邏輯;為了構(gòu)造一個(gè)能完美覆蓋的代碼步驟,我們需要構(gòu)建測(cè)試數(shù)據(jù)、Mock接口,劃分執(zhí)行順序等等,那么一旦被測(cè)試代碼發(fā)生一點(diǎn)點(diǎn)的變化都會(huì)很大程度上影響測(cè)試代碼,畢竟測(cè)試代碼都是步步依賴(lài)的;

那么我們應(yīng)該最大程度的限制由于被測(cè)試代碼的變動(dòng)而引起的測(cè)試代碼的變動(dòng),這個(gè)時(shí)候我們應(yīng)該將重構(gòu)應(yīng)用到測(cè)試代碼中;

2.1】單元測(cè)試的繼承體系(利用超類(lèi)來(lái)減少M(fèi)ock對(duì)象的使用)

將多個(gè)相關(guān)的測(cè)試用例代碼通過(guò)超類(lèi)的方式關(guān)聯(lián)起來(lái)統(tǒng)一管理將大大減少重復(fù)代碼的構(gòu)建;就跟我們重構(gòu)普通代碼一樣,將多個(gè)類(lèi)之間共享的邏輯代碼或者對(duì)象提取出來(lái)放到基類(lèi)中;這當(dāng)然也同樣適用于測(cè)試代碼,只不過(guò)需要控制一些更測(cè)試相關(guān)的邏輯;

其實(shí)大部分重復(fù)的代碼就是Mock接口的過(guò)程,我們需要將它的Mock過(guò)程精簡(jiǎn)化,但是又不能太過(guò)于精簡(jiǎn),一切精簡(jiǎn)的過(guò)程都是需要犧牲可觀察性;我們需要適當(dāng)?shù)钠胶馓崛〕鰜?lái)的對(duì)象個(gè)數(shù),將它們放入基類(lèi)中,然后在Mock的時(shí)候能通過(guò)一個(gè)簡(jiǎn)單的方法就能獲取到一個(gè)Mock過(guò)后的對(duì)象;

下面我們來(lái)看一下提取公共部分到基類(lèi)的一個(gè) 簡(jiǎn)單過(guò)程,當(dāng)然對(duì)于大項(xiàng)目而言不一定具有說(shuō)服力,就當(dāng)拋磚引玉吧;

2.1.1】公用的Mock對(duì)象

首要的任務(wù)就是將公共的Mock接口提取出來(lái),因?yàn)檫@一類(lèi)接口是肯定會(huì)在各個(gè)用例中共享的,提取過(guò)程過(guò)主要分為兩個(gè)重構(gòu)過(guò)程;

第一:將用例中的公用接口放到類(lèi)的聲明中,供所有用例使用;

第二:如果需要將公用接口提供給其他的單元測(cè)試使用,就需要提取出相關(guān)的測(cè)試基類(lèi);

我們先來(lái)看一下第一個(gè)過(guò)程,看一下測(cè)試示例代碼:

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
namespace UnitTestRefactoring
{
    public class OrderService
    {
        private IServiceConnection ServiceConnection;
        private IServiceReader ServiceReader;
        private IServiceWriter ServiceWrite;
        public OrderService(IServiceConnection connection, IServiceReader reader, IServiceWriter writer)
        {
            this.ServiceConnection = connection;
            this.ServiceReader = reader;
            this.ServiceWrite = writer;
        }
        public bool GetOrders(string orderId)
        {
            if (string.IsNullOrWhiteSpace(orderId))
                return false;
            return true;
        }
    }
}

這個(gè)類(lèi)表示遠(yuǎn)程O(píng)rder服務(wù),只有一個(gè)方法GetOrders,該方法可以根據(jù)OrderId來(lái)查詢(xún)Order信息,為了簡(jiǎn)單起見(jiàn),如果返回true說(shuō)明服務(wù)調(diào)用成功,如果返回false表示調(diào)用失??;其中構(gòu)造函數(shù)包含了三個(gè)接口,分別用來(lái)表示不同用途的接口抽象;IServiceConnection表示對(duì)遠(yuǎn)程服務(wù)鏈接的抽象,IServiceReader表示對(duì)不同服務(wù)接口讀取的抽象,IServiceWriter表示對(duì)不同服務(wù)接口寫(xiě)入的抽象;這么做可以最大化的分解耦合;

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using UnitTestRefactoring;
namespace UnitTestRefactoring.UnitTests
{
    [TestClass]
    public class OrderService_UnitTests
    {
        [TestMethod]
        public void OrderService_GetOrders_NormalFlows()
        {
            IServiceConnection mockServiceConnection = Substitute.For<IServiceConnection>();
            IServiceReader mockServiceReader = Substitute.For<IServiceReader>();
            IServiceWriter mockServiceWriter = Substitute.For<IServiceWriter>();
            OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testOrderService.GetOrders("10293884");
            Assert.AreEqual(true, testResult);
        }
        [TestMethod]
        public void OrderService_GetOrders_OrderIdIsNull()
        {
            IServiceConnection mockServiceConnection = Substitute.For<IServiceConnection>();
            IServiceReader mockServiceReader = Substitute.For<IServiceReader>();
            IServiceWriter mockServiceWriter = Substitute.For<IServiceWriter>();
            OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testOrderService.GetOrders(string.Empty);
            Assert.AreEqual(false, testResult);
        }
    }
}

這個(gè)單元測(cè)試類(lèi)是專(zhuān)門(mén)用來(lái)測(cè)試剛才那個(gè)OrderService的,里面包括兩個(gè)GetOrders方法的測(cè)試用例;可以一目了然的看見(jiàn),這兩個(gè)測(cè)試用例代碼中都包含了對(duì)測(cè)試類(lèi)的構(gòu)造函數(shù)的參數(shù)接口Mock代碼;

圖1:

.NET重構(gòu)—單元測(cè)試重構(gòu)

像這種簡(jiǎn)單的情況下,我們只需要將公共的部分拿出來(lái)放到測(cè)試的類(lèi)中聲明,就可以公用這塊對(duì)象;

圖2:

.NET重構(gòu)—單元測(cè)試重構(gòu)

這樣可以解決內(nèi)部重復(fù)問(wèn)題,但是這里需要小心的地方是,當(dāng)我們?cè)诓煌挠美g共享部分Mock邏輯的時(shí)候可能會(huì)出現(xiàn)問(wèn)題;比如我們?cè)贠rderService_GetOrders_NormalFlows用例中,對(duì)IServiceConnection接口進(jìn)行了部分行為的Mock但是當(dāng)執(zhí)行到OrderService_GetOrders_OrderIdIsNull用例時(shí)可能是用的我們上一次的Mock邏輯;所以這里需要注意一下,當(dāng)然如果設(shè)計(jì)合理的話是不太可能會(huì)出現(xiàn)這種問(wèn)題的;單一職責(zé)原則只要滿足我們的接口是不會(huì)包含其他的邏輯在里面,也不會(huì)出現(xiàn)在不同的用例之間共存相同的接口邏輯;同時(shí)也滿足接口隔離原則,就會(huì)更加對(duì)單元測(cè)試有利;

我們接著看一下第二個(gè)過(guò)程,看一下測(cè)試示例代碼:

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
namespace UnitTestRefactoring
{
    public class ProductService
    {
        private IServiceConnection ServiceConnection;
        private IServiceReader ServiceReader;
        private IServiceWriter ServiceWrite;
        public ProductService(IServiceConnection connection, IServiceReader reader, IServiceWriter writer)
        {
            this.ServiceConnection = connection;
            this.ServiceReader = reader;
            this.ServiceWrite = writer;
        }
        public bool GetProduct(string productId)
        {
            if (string.IsNullOrWhiteSpace(productId))
                return false;
            return true;
        }
    }
}

這個(gè)是表示Product服務(wù),構(gòu)造函數(shù)中同樣和之前的OrderService一樣的參數(shù)列表,然后就是一個(gè)簡(jiǎn)單的GetProduct方法;

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using UnitTestRefactoring;
namespace UnitTestRefactoring.UnitTests
{
    [TestClass]
    public class ProductService_UnitTests
    {
        IServiceConnection mockServiceConnection = Substitute.For<IServiceConnection>();
        IServiceReader mockServiceReader = Substitute.For<IServiceReader>();
        IServiceWriter mockServiceWriter = Substitute.For<IServiceWriter>();
        [TestMethod]
        public void ProductService_GetProduct_NormalFlows()
        {
            ProductService testProductService = new ProductService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testProductService.GetProduct("5475684684");
            Assert.AreEqual(true, testResult);
        }
        [TestMethod]
        public void ProductService_GetProduct_ProductIsNull()
        {
            ProductService testProductService = new ProductService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testProductService.GetProduct(string.Empty);
            Assert.AreEqual(false, testResult);
        }
    }
}

這是單元測(cè)試類(lèi),沒(méi)有什么特別的,跟之前的OrderService一樣的邏輯;是不是發(fā)現(xiàn)兩個(gè)測(cè)試類(lèi)都在公用一組相關(guān)的接口,這里就需要我們將他們提取出來(lái)放入基類(lèi)中;

using NSubstitute;
namespace UnitTestRefactoring.UnitTests
{
    public abstract class ServiceBaseUnitTestClass
    {
        protected IServiceConnection mockServiceConnection = Substitute.For<IServiceConnection>();
        protected IServiceReader mockServiceReader = Substitute.For<IServiceReader>();
        protected IServiceWriter mockServiceWriter = Substitute.For<IServiceWriter>();
    }
}

提取出來(lái)的測(cè)試基類(lèi);

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTestRefactoring.UnitTests
{
    [TestClass]
    public class ProductService_UnitTests : ServiceBaseUnitTestClass
    {
        [TestMethod]
        public void ProductService_GetProduct_NormalFlows()
        {
            ProductService testProductService = new ProductService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testProductService.GetProduct("5475684684");
            Assert.AreEqual(true, testResult);
        }
        [TestMethod]
        public void ProductService_GetProduct_ProductIsNull()
        {
            ProductService testProductService = new ProductService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testProductService.GetProduct(string.Empty);
            Assert.AreEqual(false, testResult);
        }
    }
}

ProductService_UnitTests類(lèi);

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTestRefactoring.UnitTests
{
    [TestClass]
    public class OrderService_UnitTests : ServiceBaseUnitTestClass
    {
        [TestMethod]
        public void OrderService_GetOrders_NormalFlows()
        {
            OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testOrderService.GetOrders("10293884");
            Assert.AreEqual(true, testResult);
        }
        [TestMethod]
        public void OrderService_GetOrders_OrderIdIsNull()
        {
            OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
            bool testResult = testOrderService.GetOrders(string.Empty);
            Assert.AreEqual(false, testResult);
        }
    }
}

OrderService_UnitTests 類(lèi);

提取出來(lái)的抽象基類(lèi)能在后面的單元測(cè)試重構(gòu)中幫很大忙,也是為了后面的面向特定領(lǐng)域的單元測(cè)試框架做要基礎(chǔ)工作;由于不同的單元測(cè)試類(lèi)具有不同的基類(lèi),這里需要我們自己的分析抽象,比如這里跟Service相關(guān)的,可能還有跟Order處理流程相關(guān)的,相同的一組接口也只能出現(xiàn)在相關(guān)的測(cè)試類(lèi)中;

2.1.2】公用的Mock行為

前面2.1.1】小結(jié),我們講了Mock接口對(duì)象的重構(gòu),這一節(jié)我們將來(lái)分析一下關(guān)于Mock對(duì)象行為的重構(gòu);在上面的IServiceConnection中我們加入了一個(gè)Open方法,用來(lái)打開(kāi)遠(yuǎn)程鏈接;

/*==============================================================================
* Author:深度訓(xùn)練
* Create time: 2013-10-06
* Blog Address:http://www.cnblogs.com/wangiqngpei557/
* Author Description:特定領(lǐng)域軟件工程實(shí)踐;
* ==============================================================================*/
namespace UnitTestRefactoring
{
    public interface IServiceConnection
    {
        bool Open();
    }
}

如果返回true表示遠(yuǎn)程鏈接成功建立并且已經(jīng)成功打開(kāi),如果返回false表示鏈接失??;那么在每一個(gè)用例代碼中,只要使用到了IServiceConnection接口都會(huì)需要Mock接口的Open方法;

[TestMethod]
       public void OrderService_GetOrders_NormalFlows()
       {
           mockServiceConnection.Open().Returns(true);
           mockServiceConnection.Close().Returns(true);
           OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
           bool testResult = testOrderService.GetOrders("10293884");
           Assert.AreEqual(true, testResult);
       }
       [TestMethod]
       public void OrderService_GetOrders_OrderIdIsNull()
       {
           mockServiceConnection.Open().Returns(true);
           mockServiceConnection.Close().Returns(false);
           OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);
           bool testResult = testOrderService.GetOrders(string.Empty);
           Assert.AreEqual(false, testResult);
       }

類(lèi)似這樣的代碼會(huì)很多,如果這個(gè)時(shí)候我們需要每次都在用例中對(duì)三個(gè)接口都進(jìn)行類(lèi)似的重復(fù)代碼也算是一種地效率的重復(fù)勞動(dòng),并且在后面的改動(dòng)中會(huì)很費(fèi)事;所以這個(gè)時(shí)候抽象出來(lái)的基類(lèi)就派上用場(chǎng)了,我們可以將構(gòu)建接口的邏輯代碼放入基類(lèi)中進(jìn)行統(tǒng)一構(gòu)造;

public abstract class ServiceBaseUnitTestClass
{
    protected IServiceConnection mockServiceConnection = Substitute.For<IServiceConnection>();
    protected IServiceReader mockServiceReader = Substitute.For<IServiceReader>();
    protected IServiceWriter mockServiceWriter = Substitute.For<IServiceWriter>();
    protected void InitMockServiceConnection()
    {
        this.mockServiceConnection.Open().Returns(true);
        this.mockServiceConnection.Close().Returns(true);
    }
}

this.InitMockServiceConnection();
OrderService testOrderService = new OrderService(mockServiceConnection, mockServiceReader, mockServiceWriter);

這樣在需要修改接口的時(shí)候很容易找到,可能這里兩三個(gè)用例,而且用例代碼也很簡(jiǎn)單所以看起來(lái)沒(méi)有太多的必要,但是實(shí)際情況沒(méi)有這么簡(jiǎn)單;

2.1.3】公用的Mock數(shù)據(jù)

說(shuō)到Mock數(shù)據(jù),其實(shí)需要解釋一下,準(zhǔn)確點(diǎn)講是Mock時(shí)需要用到的測(cè)試數(shù)據(jù),它是碎片化的簡(jiǎn)單的測(cè)試數(shù)據(jù);它也同樣存在著和2.1.2】小結(jié)的修改問(wèn)題,實(shí)踐告訴我單元測(cè)試代碼在整個(gè)開(kāi)發(fā)周期中最易被修改,當(dāng)我們簡(jiǎn)單的修改一個(gè)邏輯之后就需要面臨著大面積的單元測(cè)試代碼修改而測(cè)試數(shù)據(jù)修改占比重最大;

因?yàn)闇y(cè)試數(shù)據(jù)相對(duì)沒(méi)有靈活性,但是測(cè)試數(shù)據(jù)的結(jié)構(gòu)易發(fā)生由需求帶來(lái)的變化;比如實(shí)體的屬性類(lèi)型,在我們編寫(xiě)實(shí)體測(cè)試數(shù)據(jù)的時(shí)候我們用的是String,一段時(shí)間過(guò)后,實(shí)體發(fā)生變化很正常;領(lǐng)域模型在開(kāi)發(fā)周期中被修改的次數(shù)那是無(wú)法估計(jì),因?yàn)槲覀兊捻?xiàng)目中是需要迭代重構(gòu)的,我們需要重構(gòu)來(lái)為我們的項(xiàng)目保證最高的質(zhì)量;

所以單元測(cè)試修改的次數(shù)和重構(gòu)的次數(shù)應(yīng)該是成1:0的這樣的比例,修改的范圍那就不是1:10了,有時(shí)候甚至是幾何的倍數(shù);

OrderService中的AddOrder方法:

public bool AddOrder(Order order)
       {
           if (string.IsNullOrWhiteSpace(order.OrderId))
               return false;
           return true;
       }

OrderService_AddOrder測(cè)試代碼:

[TestMethod]
       public void OrderService_AddOrder_NormalFlows()
       {
           this.InitMockServiceConnection();
           OrderService testOrderService = new OrderService(this.mockServiceConnection, this.mockServiceReader, this.mockServiceWriter);
           Order testOrder = new Order() { OrderId = "123456", SubmitDT = DateTime.Now };
           bool testResult = testOrderService.AddOrder(testOrder);
           Assert.AreEqual(true, testResult);
       }
       [TestMethod]
       public void OrderService_AddOrder_OrderIdIsNull()
       {
           this.InitMockServiceConnection();
           OrderService testOrderService = new OrderService(this.mockServiceConnection, this.mockServiceReader, this.mockServiceWriter);
           Order testOrder = new Order() { OrderId = string.Empty, SubmitDT = DateTime.Now };
           bool testResult = testOrderService.AddOrder(testOrder);
           Assert.AreEqual(false, testResult);
       }

這是兩個(gè)用例,用來(lái)對(duì)AddOrder方法進(jìn)行測(cè)試,里面都包含了一條Order testOrder = new Order() 這樣的測(cè)試數(shù)據(jù)的構(gòu)造;Order實(shí)體是一個(gè)比較簡(jiǎn)單的對(duì)象,屬性也就只有兩個(gè),但是真實(shí)環(huán)境中不會(huì)這么簡(jiǎn)單,會(huì)有幾十個(gè)字段都需要進(jìn)行測(cè)試驗(yàn)證,再加上N多個(gè)用例,會(huì)使相同的代碼變的很多;

那么我們同樣需要將這部分的代碼提取出來(lái)放到基類(lèi)中去,適當(dāng)?shù)牧粲锌臻g讓用例中修改的特殊的字段;

完整的實(shí)體構(gòu)造:

Order testOrder = this.InitNormalOrder();

測(cè)試OrderId為空的邏輯,需要手動(dòng)設(shè)置為String.Empty:

Order testOrder = this.InitNormalOrder();
testOrder.OrderId = string.Empty;

這樣慢慢的就會(huì)形成抗變化的測(cè)試代碼結(jié)構(gòu),盡管一開(kāi)始很別扭,將一些直觀的對(duì)象提取出來(lái)放入一眼看不見(jiàn)的地方是有點(diǎn)不太舒服,但是長(zhǎng)遠(yuǎn)看來(lái)值得這么做;

3】LINQ表達(dá)式的重構(gòu)寫(xiě)法(將必要的LINQ寫(xiě)成普通的Function穿插在LINQ表達(dá)式中)

在使用LINQ語(yǔ)法編寫(xiě)代碼的時(shí)候,現(xiàn)在發(fā)現(xiàn)最大的問(wèn)題就是單元測(cè)試不太方便,LINQ寫(xiě)起來(lái)很方便,確實(shí)是個(gè)很不錯(cuò)的編程思想,在面對(duì)集合類(lèi)型的操作時(shí)確實(shí)是無(wú)法形容的優(yōu)雅,但是面對(duì)單元測(cè)試的問(wèn)題需要解決才行,所以需要我們平衡一下在什么情況下需要將LINQ表達(dá)式替換成普通的Function來(lái)支持;

LINQ在面對(duì)集合類(lèi)型的時(shí)候,能發(fā)揮很大的作用;不僅在Linq to Object中,在其他的Linq to Provider中都能在LINQ中找到了合適的使用之地;比如在對(duì)遠(yuǎn)程Service進(jìn)行LINQ設(shè)計(jì)的時(shí)候,我們都是按照這樣的方式進(jìn)行編寫(xiě),但是就怕LINQ中帶有邏輯判斷的表達(dá)式,這個(gè)時(shí)候就會(huì)在單元測(cè)試中總是無(wú)法覆蓋到的情況出現(xiàn),所以就需要將它提取出來(lái)使用普通的函數(shù)進(jìn)行替代;

我們來(lái)繼續(xù)看一下如果使用提取出來(lái)的函數(shù)解決鏈?zhǔn)降呐袛啵€是使用上面的OrderService為例:

public Order SelectByOrderId(string orderId)
{
    List<Order> orders = new List<Order>()
    {
         new Order(){ OrderId="123", SubmitDT=DateTime.Now.AddDays(1)},
         new Order(){ OrderId="234"}
    };
    var list = orders.Where(order => order.OrderId == orderId && order.SubmitDT > DateTime.Now);
    if (list.Count() > 0)
        return list.ToList()[0];
    return null;
}

這是一個(gè)根據(jù)OrderId獲取Order實(shí)例的方法,純粹為了演示;首先構(gòu)造了一個(gè)測(cè)試集合,然后使用了Where擴(kuò)展方法來(lái)選擇集合中滿足條件的Order;我們的重點(diǎn)是Where中的條件,條件的第一個(gè)表達(dá)式很簡(jiǎn)單而第二個(gè)表達(dá)式是SubmitDT必須大于當(dāng)前的日期,還會(huì)有很多類(lèi)似這樣的判斷,這樣測(cè)試起來(lái)很困難,而且很難維護(hù),所以我們有必要將他們提取出來(lái);

public Order SelectByOrderId(string orderId)
{
    List<Order> orders = new List<Order>()
    {
         new Order(){ OrderId="123", SubmitDT=DateTime.Now.AddDays(1)},
         new Order(){ OrderId="234"}
    };
    var list = orders.Where(order => IfOrderSubmitAndOrderId(order, orderId));
    if (list.Count() > 0)
        return list.ToList()[0];
    return null;
}
private bool IfOrderSubmitDt(Order order)
{
    return order.SubmitDT > DateTime.Now;
}
private bool IfOrderSubmitAndOrderId(Order order, string orderId)
{
    return order.OrderId == orderId && this.IfOrderSubmitDt(order);
}

其實(shí)這很像企業(yè)架構(gòu)模式中的規(guī)約模式,將規(guī)則對(duì)象化后就能隨便的控制他們,當(dāng)然這里是提取出方法,如果是大型企業(yè)級(jí)項(xiàng)目對(duì)這些易變化的點(diǎn)是需要抽取出來(lái)的;

總之遇到這樣的情況就使用簡(jiǎn)單的提取方法的方式將復(fù)雜的邏輯提取出來(lái),這也是《重構(gòu)》中的重構(gòu)策略的首要的模式;

4.面向特定領(lǐng)域的單元測(cè)試框架(一切原則即是領(lǐng)域驅(qū)動(dòng))

領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)已經(jīng)不是什么新鮮的話題了,它已經(jīng)被我們或多或少的使用過(guò),它強(qiáng)調(diào)一切從領(lǐng)域出發(fā);那么特定領(lǐng)域單元測(cè)試框架是一個(gè)什么樣的框架呢,需要的價(jià)值在哪里;其實(shí)從特定領(lǐng)域開(kāi)發(fā)框架,特定領(lǐng)域架構(gòu)我們能簡(jiǎn)單的體會(huì)到一絲意思,面向特定領(lǐng)域單元測(cè)試框架是在單元測(cè)試框架的基礎(chǔ)之上進(jìn)行二次領(lǐng)域相關(guān)的封裝;比如:如何很好的將領(lǐng)域規(guī)則獨(dú)立起來(lái),如果在單元測(cè)試中使用這些獨(dú)立起來(lái)的領(lǐng)域規(guī)則;

其實(shí)在軟件開(kāi)發(fā)的任何一個(gè)角落都能找到領(lǐng)域驅(qū)動(dòng)的影子,這也是為什么領(lǐng)域驅(qū)動(dòng)會(huì)得到我們認(rèn)可的重要因素;如果一切都圍繞著領(lǐng)域模型來(lái)的話,那么任何一個(gè)概念都不會(huì)牽強(qiáng)的,我們只有關(guān)注領(lǐng)域本身才能使軟件真的很有價(jià)值,而不是一堆代碼;

下面我們來(lái)簡(jiǎn)單的看一下 面向特定領(lǐng)域測(cè)試框架 的兩個(gè)基本功能:

4.1.分散測(cè)試邏輯、日志記錄(讓測(cè)試邏輯可以重組,記錄形式為領(lǐng)域模型)

測(cè)試代碼執(zhí)行到最后是需要對(duì)其執(zhí)行的結(jié)果進(jìn)行斷言的,如:Assert.IsTrue(testResult.SubmitDT > DateTime.Now);像這樣的一段代碼我們可以適當(dāng)?shù)陌bAssert.IsTrue方法,讓他在驗(yàn)證這段邏輯的時(shí)候能識(shí)別出領(lǐng)域概念,比如:“Order的提交時(shí)間大于今天的時(shí)間”,我們可以從兩方面入手,一個(gè)是領(lǐng)域的抽象,一個(gè)是規(guī)則的分解;

如果這里的驗(yàn)證不通過(guò),我們實(shí)時(shí)的記錄領(lǐng)域的概念到日志系統(tǒng),而不是報(bào)告那里代碼出問(wèn)題,這樣就算不是自己寫(xiě)的代碼都能一目了然;

4.2.測(cè)試用例的數(shù)據(jù)重用(為自動(dòng)化測(cè)試準(zhǔn)備固定數(shù)據(jù),建立Assert的比較測(cè)試數(shù)據(jù))

同樣比較重要的領(lǐng)域概念就是領(lǐng)域數(shù)據(jù),領(lǐng)域數(shù)據(jù)也是單元測(cè)試中用例數(shù)據(jù);為了能讓測(cè)試進(jìn)行自動(dòng)化測(cè)試,我們需要維護(hù)一組相對(duì)固定的測(cè)試數(shù)據(jù)來(lái)供測(cè)試程序運(yùn)行;其實(shí)如果想最大化建立領(lǐng)域測(cè)試框架有必要開(kāi)發(fā)一套專(zhuān)門(mén)的領(lǐng)域測(cè)試工具,它能夠?qū)崟r(shí)的讀取真實(shí)數(shù)據(jù)進(jìn)行Assert,也就更加的接近自動(dòng)化測(cè)試;

但是單元測(cè)試也不需要對(duì)真實(shí)數(shù)據(jù)進(jìn)行驗(yàn)證,真實(shí)數(shù)據(jù)一般是集成測(cè)試的時(shí)候使用的,如果能用真實(shí)數(shù)據(jù)進(jìn)行邏輯測(cè)試還是很有保障的;


作者:王清培

出處:http://wangqingpei557.blog.51cto.com/

本文版權(quán)歸作者和51CTO共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI