正文

作者水平有限,如有錯誤或紕漏,請指出,謝謝。

回到頂部

背景介紹

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

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

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

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

注:有關單元測試的定義,見另外一篇帖子,單元測試有毒

那么問題就來了,如何才能做數(shù)據的隔離呢?說一下我們的方案。

回到頂部

準備數(shù)據

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

回到頂部

測試需要注意的要點

四個函數(shù)

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

[ClassInitialize]public static void ClassInitialize(TestContext testContext){
    ...
}
[ClassCleanup]public static void ClassCleanup(){
   ...
}
[TestInitialize()]public void TestInitialize(){
   ...
}
[TestCleanup()]public void TestCleanup(){            
   ...
}
  • 顧名思義,ClassInitialize() 是在每個類初始化的時候被調用的

  • ClassCleanup() 是在類結束的時候,也就是一個類所有的case跑完的時候被調用的

  • TestInitialize() 是在每個case跑之前被調用的。

  • TestCleanup() 是在每個case調用之后被調用的。

對么?粗體的這句話不對,其余是對的。

測試用例的運行是無序的,包含多個類的情況。

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

AssemblyInitializeTestClass1: ClassInitializeTestClass1: TestInitializeTestClass1: MyTestCase1TestClass1: TestCleanupTestClass2: ClassInitializeTestClass2: TestInitializeTestClass2: MyTestCase2TestClass2: TestCleanupTestClass1: ClassCleanupTestClass2: ClassCleanup
AssemblyCleanup

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

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

三個Action

還是看下面的一個例子:

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

每個測試用例中都會有三個action,這三個Action的用途如下:

  • PretestAction做的是測試前的準備工作,具體過程中可以為每個特定的case插入或更新測試需要的數(shù)據。

  • TestAction為調用存儲過程進行測試,將實際結果和預期結果進行對比。

  • PosttestAction做的是測試完成后的清理工作,這里可以對PretestAction中的插入或者更新的數(shù)據進行回滾,恢復初始環(huán)境

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

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

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

回到頂部

總結

具體的流程就說完了,總結一下:

準備數(shù)據庫

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

運行測試用例流程

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

數(shù)據清理的兩種方法

  • 在PretestAction中添加數(shù)據恢復語句;

  • 在TestCleanup()中restore DB。

回到頂部

實例

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

創(chuàng)建一個簡單的數(shù)據庫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)建一個存儲過程

create procedure GetBasicRevenueByName(@name nvarchar(50))  
asbegin
    select bi.Name,r.BasicRevenue from EmployeeRevenue r join EmployeeBasicInfo bi on r.EmployeeNo = bi.EmployeeNo where bi.Name = @nameend

創(chuàng)建UT工程

  • 點擊File->New->Project...

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

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

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

添加一個類

  • 右鍵DBUTDemo->Add->New Item...
    iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓
    選擇SQL Server Unit Test,輸入名字,點擊Add。
    iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  • 第一次添加數(shù)據庫測試類需要配置數(shù)據庫:
    點擊New Connection

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

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

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

創(chuàng)建三個Actions

點擊Click here to create來創(chuàng)建TestAction,點擊之后發(fā)現(xiàn)多了一個resx文件。

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

輸入下面的測試代碼:

declare @return_value  int,
        @name  nvarchar(50)

EXEC    @return_value = [dbo].[GetBasicRevenueByName]
        @name = N'three zhang'SELECT  'Return Value' = @return_value

接下來創(chuàng)建另外兩個Action:

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

分別輸入如下代碼:

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 EmployeeRevenuedelete from EmployeeBasicInfo

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

最后添加測試條件

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓
我們添加了兩個測試條件,值可以在屬性界面中修改:
第一個測試條件是在返回結果集1中,第一行第二列的期望值為30000,也就是three zhang的基本工資為30000。

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

第二個測試條件測試結果集1非空。

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

編譯,運行

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

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

Ordered Test

最后說下數(shù)據庫測試用例如果需要固定的順序該怎么辦,微軟提供了一種測試用例類型叫做Ordered Test:
iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

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

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓


作者: HarlanC 

博客地址: http://www.cnblogs.com/harlanc/ 
個人博客: http://www.harlancn.me/ 
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接 . 

http://www.cnblogs.com/harlanc/p/7007145.html