<ruby id="h6500"><table id="h6500"></table></ruby>
    1. <ruby id="h6500"><video id="h6500"></video></ruby>
          1. <progress id="h6500"><u id="h6500"><form id="h6500"></form></u></progress>

            談一下我們是怎么做數據庫單元測試(Database Unit Test)的

            發(fā)表于:2017-06-15來(lái)源:未知作者:HarlanC點(diǎn)擊數: 標簽:數據庫
            這篇博文中首先介紹一下在我們的特定項目場(chǎng)景中是如何搭建DB 單元測試框架的,然后舉一個(gè)簡(jiǎn)單的例子,從頭到尾在visual studio中創(chuàng )建一個(gè)簡(jiǎn)單的單元測試工程。

            背景介紹

            最近在團隊在做release之前的regression,把各個(gè)feature分支merge回master之后發(fā)現DB的單元測試出現了20多個(gè)失敗的test cases。之前沒(méi)怎么做過(guò)DB的單元測試,正好借這個(gè)機會(huì )熟悉一下寫(xiě)DB單元測試的流程。

            這篇博文中首先介紹一下在我們的特定項目場(chǎng)景中是如何搭建DB 單元測試框架的,然后舉一個(gè)簡(jiǎn)單的例子,從頭到尾在visual studio中創(chuàng )建一個(gè)簡(jiǎn)單的單元測試工程。

            我們開(kāi)發(fā)的產(chǎn)品使用的數據庫為Sql Server,總共有400多張表,2000多個(gè)存儲過(guò)程,每個(gè)存儲過(guò)程都相當于應用代碼中的一個(gè)功能函數。代碼中的每個(gè)復雜的功能函數都可以通過(guò)寫(xiě)單元測試來(lái)在一定程度上保證代碼質(zhì)量,存儲過(guò)程也如此。代碼中的UT難點(diǎn)在于解耦,也就把相互牽連在一起的代碼彼此分離開(kāi)來(lái),各個(gè)擊破,例如A函數需要B函數提供的數據,測試A函數的時(shí)候我們只想測試A函數,不想調用B,這時(shí)候就需要我們自己提供B函數生成的數據。這叫做mock。

            在做DB單元測試的時(shí)候,存儲過(guò)程所使用的數據比較特殊,都是持久化在數據庫表中的,2000多個(gè)存儲過(guò)程增刪改查400多個(gè)表,我們需要把這些表的數據為每個(gè)存儲過(guò)程做隔離,如果測試用例使用的數據相互之間關(guān)聯(lián),恐怕會(huì )天下大亂,因為在一般情況下,單元測試用例的運行順序都是隨機的,如果單元測試使用的數據有關(guān)聯(lián),很有可能兩次運行結果也是隨機的(但是有一種方法可以固定case執行順序,我在最后的例子中進(jìn)行說(shuō)明),我們這次的20多個(gè)失敗的cases就有這種原因導致的,兩臺機器上跑出的結果不一樣,有的成功,有的失敗。

            注:有關(guān)單元測試的定義,見(jiàn)另外一篇帖子,單元測試有毒

            那么問(wèn)題就來(lái)了,如何才能做數據的隔離呢?說(shuō)一下我們的方案。

            準備數據

            我們創(chuàng )建了一個(gè)基準的數據庫,做出一個(gè)備份,叫做base.bak,這個(gè)版本比較低,比如是2.8,這里面包含了一些測試的基本數據。然后我們創(chuàng )建了另外一個(gè)preparation的工程,用于把base.bak升級到當前release版本,例如,當前release的版本為2.18。這個(gè)工程同時(shí)也測試了升級的流程。升級成功之后,把這個(gè)數據庫在本地做一個(gè)備份release_2_18.bak。好了,數據都準備好了。

            測試需要注意的要點(diǎn)

            四個(gè)函數

            對于微軟的這個(gè)DB UT測試框架,有四個(gè)函數需要搞清楚,因為這可能影響你的測試結果:

            [ClassInitialize]
            public static void ClassInitialize(TestContext testContext)
            {
                ...
            }
            [ClassCleanup]
            public static void ClassCleanup()
            {
               ...
            }
            [TestInitialize()]
            public void TestInitialize()
            {
               ...
            }
            [TestCleanup()]
            public void TestCleanup()
            {            
               ...
            }
            • 顧名思義, ClassInitialize() 是在每個(gè)類(lèi)初始化的時(shí)候被調用的
            • ClassCleanup() 是在類(lèi)結束的時(shí)候,也就是一個(gè)類(lèi)所有的case跑完的時(shí)候被調用的 。
            • TestInitialize() 是在每個(gè)case跑之前被調用的。
            • TestCleanup() 是在每個(gè)case調用之后被調用的。

            對么?粗體的這句話(huà)不對,其余是對的。

            測試用例的運行是無(wú)序的,包含多個(gè)類(lèi)的情況。

            看下面測試用例的之情情況你就明白了:

            AssemblyInitialize
            TestClass1: ClassInitialize
            TestClass1: TestInitialize
            TestClass1: MyTestCase1
            TestClass1: TestCleanup
            TestClass2: ClassInitialize
            TestClass2: TestInitialize
            TestClass2: MyTestCase2
            TestClass2: TestCleanup
            TestClass1: ClassCleanup
            TestClass2: ClassCleanup
            AssemblyCleanup

            ClassCleanup() 并不意味著(zhù) TestClass1 的 ClassCleanup 在這個(gè)類(lèi)的最后一個(gè)case跑完之后被立即調用!事實(shí)上,它會(huì )等待所有case都被運行完之后,同 TestClass2 的 ClassCleanup 一塊執行。

            具體原因看這個(gè)帖子, How to run ClassCleanup (MSTest) after each class with test?

            三個(gè)Action

            還是看下面的一個(gè)例子:

            [TestMethod()]
            public void Test_GetBasicRevenueByName()
            {
                SqlDatabaseTestActions testActions = this.SqlTest1Data;
                // Execute the pre-test script
                // 
                System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script...");
                SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction);
                // Execute the test script
                // 
                System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script...");
                SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction);
                // Execute the post-test script
                // 
                System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script...");
                SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction);
            }

            每個(gè)測試用例中都會(huì )有三個(gè)action,這三個(gè)Action的用途如下:

            • PretestAction做的是測試前的準備工作,具體過(guò)程中可以為每個(gè)特定的case插入或更新測試需要的數據。
            • TestAction為調用存儲過(guò)程進(jìn)行測試,將實(shí)際結果和預期結果進(jìn)行對比。
            • PosttestAction做的是測試完成后的清理工作,這里可以對PretestAction中的插入或者更新的數據進(jìn)行回滾, 恢復初始環(huán)境 。

            最后的這個(gè)PosttestAction為我們的數據隔離提供了一種方法,所謂恢復初始環(huán)境的意思是執行一個(gè)case之前和之后數據庫中的數據完全一樣。

            這里有個(gè)問(wèn)題,在PretestAction中進(jìn)行數據插入還比較好恢復,如果是刪除和更新呢?這就需要你記錄下刪除的和更新前的數據。太麻煩了。如果你的系統性能足夠好,或者對運行UT的時(shí)間沒(méi)有要求,可以用另外一種方法: restore DB 。前面不是說(shuō)過(guò)了么,我們在數據庫升級之后做了一個(gè)備份,我們在這里使用它。在什么地方執行restoreDB?對,在 TestCleanup() 中進(jìn)行。

            [TestInitialize()]
            public void TestCleanup()
            {
               restoreDB();
            }

            總結

            具體的流程就說(shuō)完了,總結一下:

            準備數據庫

            運行測試用例流程

            數據清理的兩種方法

            • 在PretestAction中添加數據恢復語(yǔ)句;
            • 在 TestCleanup() 中restore DB。

            實(shí)例

            接下來(lái)我們從頭到尾演示一下用VS2013 + SQL Server 2012是如何做數據庫UT的。

            創(chuàng )建一個(gè)簡(jiǎn)單的數據庫 DBUTDemo

            • 創(chuàng )建兩張表。
            create table EmployeeBasicInfo(
               EmployeeNo int NOT NULL primary key,
               Name nvarchar(50) NOT NULL,
               TelephoneNum varchar(50) NOT NULL  
            );
            
            create table EmployeeRevenue(
               EmployeeNo int NOT NULL primary key,
               BasicRevenue int NOT NULL,
               MealSubsidy int NULL,
               Bonus int NULL,
               foreign key(EmployeeNo) references EmployeeBasicInfo(EmployeeNo)  
            );
            • 創(chuàng )建一個(gè)存儲過(guò)程
            create procedure GetBasicRevenueByName(@name nvarchar(50))  
            as
            begin
                select bi.Name,r.BasicRevenue from EmployeeRevenue r join EmployeeBasicInfo bi on r.EmployeeNo = bi.EmployeeNo where bi.Name = @name
            end

            創(chuàng )建UT工程

            • 點(diǎn)擊 File->New->Project...

            • 選擇 Unit Test Project ,輸入工程名,選擇創(chuàng )建路徑,點(diǎn)擊 OK 。

            添加一個(gè)類(lèi)

            • 右鍵 DBUTDemo->Add->New Item... 
              選擇 SQL Server Unit Test ,輸入名字,點(diǎn)擊Add。 
            • 第一次添加數據庫測試類(lèi)需要配置數據庫: 
              點(diǎn)擊 New Connection 。

            輸入 Server name ,選擇我們剛才創(chuàng )建的數據庫 DBUTDemo ,點(diǎn)擊 Test Connection 。如果成功會(huì )彈出對話(huà)框。連續兩次點(diǎn)擊OK。數據庫配置就完成了。

            創(chuàng )建三個(gè)Actions

            點(diǎn)擊 Click here to create 來(lái)創(chuàng )建TestAction,點(diǎn)擊之后發(fā)現多了一個(gè)resx文件。

            輸入下面的測試代碼:

            declare @return_value  int,
                    @name  nvarchar(50)
            
            EXEC    @return_value = [dbo].[GetBasicRevenueByName]
                    @name = N'three zhang'
            
            SELECT  'Return Value' = @return_value

            接下來(lái)創(chuàng )建另外兩個(gè)Action:

            分別輸入如下代碼:

            insert into EmployeeBasicInfo values(1,'three zhang',    '16625344257')
            insert into EmployeeBasicInfo values(2,'four li',   '16625344258')
            insert into EmployeeBasicInfo values(3,'simon', '16625344259')
            insert into EmployeeBasicInfo values(4,'jack',  '16625344250')
            
            insert into EmployeeRevenue values(1    ,30000  ,500    ,20000)
            insert into EmployeeRevenue values(2    ,28000  ,500    ,19000)
            insert into EmployeeRevenue values(3    ,27000  ,500    ,10000)
            insert into EmployeeRevenue values(4    ,26000  ,500    ,20000)
            delete from EmployeeRevenue
            delete from EmployeeBasicInfo

            最后添加測試條件

            我們添加了兩個(gè)測試條件,值可以在屬性界面中修改:

            第一個(gè)測試條件是在返回結果集1中,第一行第二列的期望值為30000,也就是three zhang的基本工資為30000。

            第二個(gè)測試條件測試結果集1非空。

            編譯,運行

            編譯成功后,打開(kāi)Test Explorer,run我們剛才創(chuàng )建的case,測試通過(guò)。

            Ordered Test

            最后說(shuō)下數據庫測試用例如果需要固定的順序該怎么辦,微軟提供了一種測試用例類(lèi)型叫做Ordered Test:

            這種case是把幾個(gè)case集合成為了一個(gè),可以自己選擇需要運行的普通的case,自己指定順序。因為順序固定了,這些cases中使用的數據就是可控的,因此在一個(gè)ordered case中的幾個(gè)case可以共同使用某些數據,我們可以將數據隔離的單位由單個(gè)case變?yōu)閹讉€(gè)case甚至一個(gè)類(lèi)中的所有cases。

            原文轉自:http://www.cnblogs.com/harlanc/p/7007145.html

            老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月
              <ruby id="h6500"><table id="h6500"></table></ruby>
              1. <ruby id="h6500"><video id="h6500"></video></ruby>
                    1. <progress id="h6500"><u id="h6500"><form id="h6500"></form></u></progress>