最近在團隊在做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就有這種原因導致的,兩臺機器上跑出的結果不一樣,有的成功,有的失敗。
那么問(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。好了,數據都準備好了。
對于微軟的這個(gè)DB UT測試框架,有四個(gè)函數需要搞清楚,因為這可能影響你的測試結果:
[ClassInitialize]
public static void ClassInitialize(TestContext testContext)
{
...
}
[ClassCleanup]
public static void ClassCleanup()
{
...
}
[TestInitialize()]
public void TestInitialize()
{
...
}
[TestCleanup()]
public void TestCleanup()
{
...
}
對么?粗體的這句話(huà)不對,其余是對的。
看下面測試用例的之情情況你就明白了:
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è)例子:
[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的用途如下:
這里有個(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ō)完了,總結一下:
接下來(lái)我們從頭到尾演示一下用VS2013 + SQL Server 2012是如何做數據庫UT的。
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)
);
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
輸入 Server name ,選擇我們剛才創(chuàng )建的數據庫 DBUTDemo ,點(diǎn)擊 Test Connection 。如果成功會(huì )彈出對話(huà)框。連續兩次點(diǎn)擊OK。數據庫配置就完成了。
點(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ò)。
最后說(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