數(shù)據(jù)庫程序的單元測試(ZT)
發(fā)表時間:2024-06-14 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]數(shù)據(jù)庫程序的單元測試本文來自互聯(lián)網(wǎng),原作者不詳。翻譯:PMTeamXu NingNing這些筆錄是我關(guān)于已完成的數(shù)據(jù)庫功能測試的一些心得。其中的例子是用java語言編寫的,但我認(rèn)為這些想法對于大多數(shù)編程環(huán)境都普遍適用。當(dāng)然,我仍致力于尋找更佳的解決方案,F(xiàn)實的問題是這樣的:你有一個SQL數(shù)據(jù)庫,一...
數(shù)據(jù)庫程序的單元測試
本文來自互聯(lián)網(wǎng),原作者不詳。
翻譯:PMTeamXu NingNing
這些筆錄是我關(guān)于已完成的數(shù)據(jù)庫功能測試的一些心得。其中的例子是用java語言編寫的,但我認(rèn)為這些想法對于大多數(shù)編程環(huán)境都普遍適用。當(dāng)然,我仍致力于尋找更佳的解決方案。
現(xiàn)實的問題是這樣的:你有一個SQL數(shù)據(jù)庫,一些存儲過程和一個介于應(yīng)用程序和數(shù)據(jù)庫之間的中間層。你怎樣在其中插入測試代碼從而保證在數(shù)據(jù)庫中數(shù)據(jù)存取功能的實現(xiàn)?
一、為什么會有這樣的問題?
我猜想有些,可能不完全是大多數(shù)的數(shù)據(jù)庫開發(fā)過程都是這樣的:建立數(shù)據(jù)庫,編寫存取數(shù)據(jù)到數(shù)據(jù)庫的代碼,編譯并運行,用一個查詢語句查驗所列的數(shù)據(jù)是否正確顯示。如果能正確顯示那就大功告成了。
然而,這種靠眼睛來檢測的弊端在于:你不經(jīng)常進(jìn)行這樣的檢驗,而且這種檢驗是不完全的。存在這樣的可能性,當(dāng)你對系統(tǒng)進(jìn)行了修改,過了幾個月后,你無意中破壞了系統(tǒng),從而導(dǎo)致數(shù)據(jù)的丟失。作為一個編程人員,你可能不會花很多時間來檢查數(shù)據(jù)本身,這就使錯誤的數(shù)據(jù)要經(jīng)過較長的時間才能暴露出來。我曾經(jīng)參與一個建立網(wǎng)站的項目,該項目中在注冊時有一個必填數(shù)據(jù)在大半年中沒有被發(fā)現(xiàn)未實際輸入進(jìn)數(shù)據(jù)庫。盡管公司市場部曾經(jīng)提出他們需要這一信息,但因為這項數(shù)據(jù)從來沒有人去看它,直接導(dǎo)致了這一問題在很長時間內(nèi)沒有被發(fā)現(xiàn)。
自動化測試,由于它能經(jīng)常測試而且測試范圍較廣,降低了數(shù)據(jù)丟失的風(fēng)險。我發(fā)現(xiàn)它能使我更心安理得地休息。當(dāng)然,自動化測試還有其他一些好處,他們本身就是代碼編寫的范例,也可以作為文檔,便于你修改別人編寫的原始程序,從而減少檢測所需的時間。
二、什么是我們所談?wù)摰臏y試?
設(shè)想有一個非常簡單的用戶數(shù)據(jù)庫,包括用戶電子信箱和一個標(biāo)志,用來指示郵件地址是否被彈回。你的數(shù)據(jù)庫程序應(yīng)該包括插入、修改、刪除和查詢等方法
插入方法會調(diào)用一個存儲過程將數(shù)據(jù)寫入數(shù)據(jù)庫。為了敘述方便,這里省去了一些細(xì)節(jié),大致的程序如下所示:
public class UserDatabase
{
...
public void insert(User user)
{
PreparedStatement ps = connection.prepareCall("{ call User_insert(?,?) }");
ps.setString(1, user.getEmail());
ps.setString(2, user.isBad());// In real life, this would be a boolean.
ps.executeUpdate();
ps.close();
}
...
}
而我認(rèn)為的測試代碼應(yīng)為:
public class TestUserDatabase extends TestCase
{
...
public void testInsert()
{
// Insert a test user:
User user = new User("some@email.address");
UserDatabase database = new UserDatabase();
database.insert(user);
// Make sure the data really got there:
User db_user = database.find("some@email.address");
assertTrue("Expected non-null result", db_user != null);
assertEquals("Wrong email", "some@email.address", db_user.getEmail());
assertEquals("Wrong bad flag", false, db_user.isBad());
}
...
}
可能你還有更多測試代碼。(注意一些測試,例如對date類的測試)。
assertTrue和assertEquals方法進(jìn)行條件測試。如果測試失敗,他們將返回診斷消息。其重點是這些測試都基于一個測試框架自動執(zhí)行,并給出測試成敗的標(biāo)志。這些測試都基于用java語言編寫的測試框架Junit類(程序附后)。這一框架也能適應(yīng)其他諸如C, C++, Perl, Python, .NET (all languages), PL/SQL, Eiffel, Delphi, VB等語言環(huán)境。
下一個問題就是:我們有測試,但我們怎樣保證測試數(shù)據(jù)和實際數(shù)據(jù)能嚴(yán)格區(qū)分?
三、不同的鑒別方法
在開始之前,我必須指出你最好有一個測試用的數(shù)據(jù)庫,你可能更想在非正式的數(shù)據(jù)庫中實踐我講的東西。
第一種方法是手工在數(shù)據(jù)庫中輸入一些預(yù)先知道的測試性數(shù)據(jù),例如在郵件地址中輸入“testuser01@test.testing”。如果你正在測試數(shù)據(jù)庫的查詢功能,你能預(yù)先知道,比如說有五個,數(shù)據(jù)庫記錄是以“@test.testing”結(jié)尾的。
由以上方式插入的數(shù)據(jù)必須由測試本身進(jìn)行必要的維護(hù)。例如,測試必須負(fù)責(zé)刪除所建立的測試數(shù)據(jù),而避免對實際數(shù)據(jù)進(jìn)行操作,從而保證整個數(shù)據(jù)庫處于完好狀態(tài)。
這種方法還是存在以下問題:
l你不得不和其他編程人員進(jìn)行數(shù)據(jù)協(xié)調(diào)——假設(shè)他們也有他們自己的測試數(shù)據(jù)庫。
l在數(shù)據(jù)庫中有些特殊的數(shù)據(jù)并不正確,如一些特別的郵件地址和被保留餓編號前綴。
l在某些情況下,你將不能用一些特殊的數(shù)據(jù)來區(qū)分測試數(shù)據(jù)和實際數(shù)據(jù),這就比較棘手。例如,某條數(shù)據(jù)由一些整數(shù)型字段構(gòu)成,而作為測試用的數(shù)值都看起來較為合理。
l你的測試只限于你為測試所保留的某些特殊值,這意味著你將小心地選擇那些特殊值。
l如果數(shù)據(jù)對時間敏感,那對數(shù)據(jù)庫的維護(hù)將更為困難。例如,數(shù)據(jù)庫中有產(chǎn)品銷售提議,而該提議只在明確的時間段里有效。
我曾經(jīng)試著做過修改。例如,在數(shù)據(jù)庫中增加“is_test”字段作為區(qū)分測試數(shù)據(jù)的標(biāo)志,從而避免特殊值的問題。但由此帶來的問題是,你的測試代碼將只測試那些標(biāo)記為測試的數(shù)據(jù),而你的正式代碼卻要處理那些未標(biāo)記為測試的數(shù)據(jù)。如果你的測試在這方面有區(qū)別,你事實上并不在測試同一代碼。
四、你需要四個數(shù)據(jù)庫
有些想法認(rèn)為一個好的測試是足夠充分的并能建立測試所需要的全部數(shù)據(jù)。如果你能在測試進(jìn)行前就明確知道數(shù)據(jù)庫所處的狀態(tài),測試可以進(jìn)行一些簡化。一個簡化的方法是建立一個獨立的單元測試數(shù)據(jù)庫用于測試程序,測試程序在開始進(jìn)行前清除測試數(shù)據(jù)庫中的全部數(shù)據(jù)。
在代碼中,你可以編寫一個dbSetUp方法,如下所示:
public void dbSetUp()
{
// Put the database in a known state:
// (stored procedures would probably be better here)
helper.exec("DELETE FROM SomeSideTable");
helper.exec("DELETE FROM User");
// Insert some commonly-used test cases:
...
}
任何數(shù)據(jù)庫測試程序都將在做任何事前首先調(diào)用dbSetUp方法,它將使測試數(shù)據(jù)庫處于一種已知狀態(tài)(大部分情況下是空數(shù)據(jù)庫狀態(tài))。這種做法具有以下的優(yōu)點:
l所有的測試數(shù)據(jù)都在代碼層和其他編程人員進(jìn)行交流,因此沒有必要進(jìn)行外部測試數(shù)據(jù)協(xié)調(diào)。
l無須測試用的特殊數(shù)據(jù)的介入。
l簡單而容易理解的一種方法。
l在每一次測試前刪除和插入數(shù)據(jù)可能會花較多時間,但是由于測試用的數(shù)據(jù)量相對較小,我認(rèn)為這種方法比較快捷,特別是在測試一個本地數(shù)據(jù)庫時。
這種做法不利的一面是你需要至少兩個數(shù)據(jù)庫。但是請記住,他們在必要是都可以在同一個服務(wù)器上運行。采用這種方法,我用了四個數(shù)據(jù)庫,另外兩個在緊急關(guān)頭時使用,具體如下:
1. 實際使用數(shù)據(jù)庫,包含實際數(shù)據(jù)。在這個數(shù)據(jù)庫中不進(jìn)行測試,確保數(shù)據(jù)的完整性。
2. 你的本地開發(fā)數(shù)據(jù)庫,用來進(jìn)行大部分的測試。
3. 一個加入一定量數(shù)據(jù)的本地開發(fā)數(shù)據(jù)庫,可能和其他編程人員共享,用來運行應(yīng)用程序并檢測是否能在實際使用的數(shù)據(jù)庫上運行,而不是照搬實際使用數(shù)據(jù)庫中的全部數(shù)據(jù)。從嚴(yán)格意義上說你可能并不需要這一數(shù)據(jù)庫,但這一數(shù)據(jù)庫能確保應(yīng)用程序在有大量數(shù)據(jù)的數(shù)據(jù)庫中順利運行。
4. 一個發(fā)布數(shù)據(jù)庫,或稱集成數(shù)據(jù)庫,用來在正式發(fā)布前進(jìn)行一系列測試,從而確保對所有本地數(shù)據(jù)庫的修改都得到確認(rèn)。如果你一個人開發(fā),你可以省略這個數(shù)據(jù)庫,但你必須確保所有對數(shù)據(jù)結(jié)構(gòu)和存儲過程的修改都在實際使用數(shù)據(jù)庫中得到確認(rèn)。
在有多個數(shù)據(jù)庫的情況下,你要確保不同數(shù)據(jù)庫間結(jié)構(gòu)的同步:如果你在測試數(shù)據(jù)庫中改變表的定義或存儲過程,你必須記得在實際使用的服務(wù)器上進(jìn)行同樣的修改。發(fā)布數(shù)據(jù)庫的作用就是提醒你進(jìn)行這些修改。另外,我發(fā)現(xiàn)如果代碼控制系統(tǒng)能將提交時的注釋用郵件形式自動發(fā)給整個開發(fā)組,那將給團(tuán)隊開發(fā)帶來較大幫助。CVS就能做到這一點,我希望你能利用這一功能。
五、在合適的數(shù)據(jù)庫中進(jìn)行測試
在這種情況下,你必須連接正確的數(shù)據(jù)庫。在實際使用數(shù)據(jù)庫中進(jìn)行測試有可能刪除所有的有用數(shù)據(jù),這點令我十分害怕。
有幾種辦法能避免此類悲劇的發(fā)生。例如,比較普遍的做法是將數(shù)據(jù)庫連接設(shè)置記錄在初始文件中,從而明確哪一個是測試數(shù)據(jù)庫。你也可以通過初始文件進(jìn)行本地數(shù)據(jù)庫的測試,而用其他指定方法連接實際使用數(shù)據(jù)庫。
在java代碼中,初始文件可能如下所示;
myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase
這一連接字符串用來連接數(shù)據(jù)庫。你可以添加第二個連接字符串來區(qū)分測試數(shù)據(jù)庫:
myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase
myapp.db.testurl=jdbc:mysql://127.0.0.1/my_test_database
在測試代碼中,你可以檢查并確保在連接到測試數(shù)據(jù)庫后應(yīng)用程序才能繼續(xù)運行:
public void dbSetUp()
{
String test_db = InitProperties.get("myapp.db.testurl");
String db = InitProperties.get("myapp.db.url");
if (test_db == null)
abort("No test database configured");
if (test_db.equals(db))
{
// All is well: the database we're connecting to is the
// same as the database identified as "for testing"
}
else
{
abort("Will not run tests against a non-test database");
}
}
另一個技巧是,如果你有一個本地測試數(shù)據(jù)庫,測試程序能通過提供IP地址或主機名進(jìn)行檢測。如果不是“l(fā)ocalhost/127.0.0.1”,這就有連接在實際使用數(shù)據(jù)庫上進(jìn)行測試的風(fēng)險。
六、關(guān)于測試日期的體會
如果你想存儲日期信息,你大概想確認(rèn)你存的日期信息是否正確。請注意以下幾點。
首先先問自己,是誰創(chuàng)建該日期。如果是你的應(yīng)用程序,那驗證比較簡單,因為你可以通過查看數(shù)據(jù)庫中的具體日期進(jìn)行比較。如果是數(shù)據(jù)庫本身創(chuàng)建該日期,可能作為一個缺省字段,那你可能就會有些問題。例如,你能確保你代碼所代表的時區(qū)和數(shù)據(jù)庫的時區(qū)一致嗎?從沒有聽說過數(shù)據(jù)庫是以格林尼治標(biāo)準(zhǔn)時間為準(zhǔn)顯示時間和日期的。你能確保運行應(yīng)用程序的計算機上的時間和數(shù)據(jù)庫所在計算機上的時間保持一致嗎?如果不是,你必須在進(jìn)行時間的比較時留出一定的誤差。
如果你遇到這些情況,有些事是你可以做的:
如果你預(yù)先知道所用的時區(qū),在測試前將所有日期和時間全部轉(zhuǎn)換成那個時區(qū)的日期和時間。
在比較時間時設(shè)立一定的誤差,比如說幾分鐘、幾小時或幾個月。看上去缺乏說服力,但至少它能捕獲諸如日期為空或1970年1月1日等錯誤。
七、總結(jié)
在本文中,我想說:
單元數(shù)據(jù)庫測試是一件值得做的事;
如果你能給一系列測試程序一個對應(yīng)的數(shù)據(jù)庫,測試本身并不非?膳。
還有其他方法能解決這一問題。我還不能確信模仿對象(Mock Object)這一方法。就我對這一方法的理解,模仿對象模擬了一個系統(tǒng)中間層(在本文中,是數(shù)據(jù)庫操作系統(tǒng)),使得模仿的數(shù)據(jù)庫總能返回你想要的數(shù)據(jù)。我比較欣賞這一概念,它鼓勵你對測試進(jìn)行分層,可能劃分成SQL方面的測試和Java語言方面的測試,從而對模仿的ResultSet對象進(jìn)行測試。
我比較關(guān)注那些能導(dǎo)致一次能使兩個或兩個以上的數(shù)據(jù)表產(chǎn)生變化的操作。在這種情況下,用模仿對象方法進(jìn)行數(shù)據(jù)庫的維護(hù)和實現(xiàn)比較困難。當(dāng)然,我還要找到一種好方法進(jìn)行數(shù)據(jù)庫中SQL方面的測試,從而確認(rèn)數(shù)據(jù)被正確地存儲到數(shù)據(jù)庫中。