對(duì)數(shù)據(jù)庫(kù)測(cè)試的根本誤解
有許多關(guān)于測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-Driven Development懒震,縮寫為TDD)的書(shū)籍。那些書(shū)通常關(guān)注的是將測(cè)試應(yīng)用于工作單元(units of work)。對(duì)于工作單元的理解有許多種不同的方式,通常它表示一個(gè)類(class)鬼廓。正如那些書(shū)中所言:編寫許多測(cè)試,以使那些測(cè)試都能通過(guò)的方式創(chuàng)建代碼致盟。應(yīng)模擬所有的外部資源碎税,以便你可以只測(cè)試這個(gè)單元。
這很酷馏锡,但不幸的是所有的測(cè)試在此刻停止了雷蹂。因?yàn)橥ǔ?huì)有些沒(méi)被測(cè)到的查詢(手寫的或者是由某些ORM工具生成的)。有些程序員使用集成測(cè)試來(lái)測(cè)試那些查詢——連接到一個(gè)真實(shí)的數(shù)據(jù)庫(kù)并執(zhí)行真實(shí)的查詢來(lái)進(jìn)行測(cè)試眷篇。這種做法通常意味著在測(cè)試快樂(lè)路徑(happy path)——我已經(jīng)有了ORM工具萎河,所以它會(huì)搞定每件事,我無(wú)須費(fèi)心。
數(shù)據(jù)庫(kù)通常是一家公司最有價(jià)值的資產(chǎn)虐杯。應(yīng)用程序可以一遍一遍重寫玛歌。舊的應(yīng)用程序扔出去,新的應(yīng)用程序裝進(jìn)來(lái)擎椰。但是更換應(yīng)用程序時(shí)沒(méi)人會(huì)丟棄滿載數(shù)據(jù)的數(shù)據(jù)庫(kù)支子。而是將數(shù)據(jù)庫(kù)小心翼翼地遷移過(guò)去。位于多個(gè)系統(tǒng)中的許多不同的應(yīng)用程序會(huì)在同一時(shí)刻使用同一數(shù)據(jù)庫(kù)达舒。這就是為什么擁有充滿約束的良好數(shù)據(jù)庫(kù)模型是如此重要值朋、以及為什么應(yīng)謹(jǐn)慎對(duì)待數(shù)據(jù)庫(kù)的原因。你真的不想破壞數(shù)據(jù)一致性(consistency)巩搏,因?yàn)檫@會(huì)使你的公司付出高昂的代價(jià)昨登。
本文是關(guān)于經(jīng)常被遺忘的數(shù)據(jù)庫(kù)測(cè)試的。使用真實(shí)數(shù)據(jù)進(jìn)行集成測(cè)試贯底。實(shí)際上丰辣,它與你所使用的數(shù)據(jù)庫(kù)引擎的類型無(wú)關(guān)緊要。你可以使用PostgreSQL禽捆、MySQL笙什、Oracle,或者甚至使用那些有趣的noSQL數(shù)據(jù)庫(kù)胚想,例如MongoDB琐凭。以下規(guī)則可適用于各種數(shù)據(jù)庫(kù)和各類應(yīng)用程序。也許不是全部浊服,例如noSQL數(shù)據(jù)庫(kù)就無(wú)法強(qiáng)制實(shí)施數(shù)據(jù)完整性(integrity)统屈。
你的應(yīng)用程序通常是由許多不同的部件組成的。其中有一些<將任何你喜歡的語(yǔ)言放在這里>代碼臼闻,一些配置文件鸿吆,一些SQL查詢,一些外部系統(tǒng)述呐。測(cè)試一個(gè)應(yīng)用程序意味著分別測(cè)試每個(gè)部件(因?yàn)橹挥羞@樣才更容易找出bug)惩淳、以及測(cè)試所有部件是如何協(xié)作的。數(shù)據(jù)庫(kù)就是這些部件的其中之一乓搬,而且你應(yīng)該徹底測(cè)試它思犁。
不測(cè)試數(shù)據(jù)庫(kù)
這是首要的、最可怕的錯(cuò)誤进肯。根本不測(cè)試數(shù)據(jù)庫(kù)激蹲。你編寫了一些使用數(shù)據(jù)庫(kù)的代碼。你甚至使用一些模擬數(shù)據(jù)庫(kù)連接為這些類創(chuàng)建了單元測(cè)試江掩。
集成測(cè)試怎么樣学辱?集成測(cè)試應(yīng)在生產(chǎn)環(huán)境下對(duì)應(yīng)用程序進(jìn)行測(cè)試乘瓤。集成測(cè)試背后的唯一想法是,確保應(yīng)用程序部署到生產(chǎn)環(huán)境后可以正常工作策泣。如果你不在生產(chǎn)數(shù)據(jù)庫(kù)上測(cè)試應(yīng)用程序衙傀,那么你實(shí)際上不并不知道應(yīng)用程序能否工作。你的模擬連接讓你可以發(fā)送尚未檢查以及沒(méi)有檢查的任何查詢萨咕。模擬連接只返回你所需的數(shù)據(jù)统抬。
不創(chuàng)建集成測(cè)試意味著你實(shí)際上沒(méi)有測(cè)試你的應(yīng)用程序。
不測(cè)試數(shù)據(jù)庫(kù)Schema(模式/架構(gòu))
我所觀察過(guò)的大多數(shù)團(tuán)隊(duì)擁有某種形式的集成測(cè)試危队。通常進(jìn)行快樂(lè)路徑測(cè)試:有某個(gè)ORM工具聪建,我們持久化對(duì)象,ORM工具會(huì)完成那些工作茫陆,真是太酷了金麸,我無(wú)須費(fèi)心。
我從未見(jiàn)過(guò)一支對(duì)數(shù)據(jù)庫(kù)schema(模式/架構(gòu))進(jìn)行測(cè)試的團(tuán)隊(duì)簿盅。想象一下钱骂,由于某些針對(duì)產(chǎn)品的查詢很慢,因此你必須在該數(shù)據(jù)庫(kù)中創(chuàng)建某個(gè)索引挪鹏。當(dāng)下次在新的客戶環(huán)境中部署此應(yīng)用程序時(shí),你希望擁有該索引并確認(rèn)該索引真的就在那里愉烙。為什么不編寫一個(gè)簡(jiǎn)單的測(cè)試來(lái)檢查該索引的存在呢讨盒?
除了索引,還有許多要測(cè)試的內(nèi)容:
- 主鍵(primary keys)
- 外鍵(foreign keys)
- 一些檢查——以確辈皆穑“price”(價(jià)格)列不會(huì)有負(fù)值
- 某些列的唯一性(uniqueness)——你實(shí)際上不想擁有兩個(gè)具有相同登錄名的用戶返顺。
不在生產(chǎn)環(huán)境下測(cè)試
當(dāng)你開(kāi)發(fā)某個(gè)應(yīng)用程序時(shí),你可以從種類繁多的數(shù)據(jù)庫(kù)中進(jìn)行選擇蔓肯。通常你會(huì)從中選擇那個(gè)最好的遂鹊、那個(gè)被團(tuán)隊(duì)所熟知的、或者是由管理層所選定的(有時(shí)使用一些奇怪的理由)蔗包。有時(shí)同一應(yīng)用程序的多個(gè)部署會(huì)在同一時(shí)間使用不同的數(shù)據(jù)庫(kù)引擎秉扑。有時(shí)應(yīng)用程序會(huì)為了能使用不同的數(shù)據(jù)庫(kù)引擎進(jìn)行準(zhǔn)備,因此購(gòu)買此應(yīng)用程序的客戶就可以選擇他想要的數(shù)據(jù)庫(kù)调限。
數(shù)據(jù)庫(kù)引擎的選擇真的與進(jìn)行產(chǎn)品測(cè)試無(wú)關(guān)舟陆。
由于程序員的懶惰,因此他們希望他們的測(cè)試可以運(yùn)行得飛快耻矮。他們不想為測(cè)試結(jié)果等太久秦躯。這也就是為什么許多團(tuán)隊(duì)使用某些更快的內(nèi)存數(shù)據(jù)庫(kù)(例如HSQLDB)的原因。由于那些內(nèi)存數(shù)據(jù)庫(kù)僅存儲(chǔ)在RAM(Random Access Memory裆装,隨機(jī)存取存儲(chǔ)器)內(nèi)存中踱承,而且在操作時(shí)不接觸任何硬盤驅(qū)動(dòng)器倡缠,因此它們的運(yùn)行速度要快得多。
還有一條常常被人遺忘的規(guī)則:
測(cè)試應(yīng)該使用與生產(chǎn)環(huán)境相同的數(shù)據(jù)庫(kù)引擎茎活。
許多程序員會(huì)使用某個(gè)其他引擎昙沦。常見(jiàn)的解釋很簡(jiǎn)單:“我們的數(shù)據(jù)庫(kù)太慢了,我們應(yīng)該使用某個(gè)內(nèi)存數(shù)據(jù)庫(kù)引擎妙色∥ψ蹋”。這并不是個(gè)好主意身辨。這樣你測(cè)試的是該其他引擎丐谋,而非你的生產(chǎn)環(huán)境中的那個(gè),所以實(shí)際上你并沒(méi)對(duì)你的應(yīng)用程序進(jìn)行測(cè)試煌珊。
我曾經(jīng)遇到過(guò)這個(gè)問(wèn)題号俐。我們必須在成功連接數(shù)據(jù)庫(kù)后通過(guò)設(shè)置session變量來(lái)優(yōu)化查詢。那個(gè)應(yīng)用程序在生產(chǎn)環(huán)境中只使用Oracle數(shù)據(jù)庫(kù)定庵。當(dāng)設(shè)置此變量以后吏饿,測(cè)試失敗了。而且是所有的測(cè)試都失敗了蔬浙。原來(lái)是我不能在HSQLDB內(nèi)存數(shù)據(jù)庫(kù)中設(shè)置此變量猪落,因?yàn)樗静淮嬖凇R虼顺氩冶仨毦帉懸欢卧愀獾拇a:在連接后笨忌,檢查數(shù)據(jù)庫(kù)引擎,并由此決定是否設(shè)置此變量俱病。
即使你沒(méi)有任何與混合引擎有關(guān)的問(wèn)題官疲,請(qǐng)記住,當(dāng)你測(cè)試其他非生產(chǎn)環(huán)境的數(shù)據(jù)庫(kù)引擎時(shí)亮隙,你恰恰根本沒(méi)對(duì)你的應(yīng)用程序進(jìn)行測(cè)試途凫。
不準(zhǔn)備好數(shù)據(jù)庫(kù)就進(jìn)行測(cè)試
測(cè)試有一個(gè)通用的、明確定義的流程溢吻。 它非常簡(jiǎn)單:
- 準(zhǔn)備環(huán)境
- 運(yùn)行一個(gè)測(cè)試
- 檢查測(cè)試結(jié)果
- 返回至第一點(diǎn)
若嘗試背道而馳维费,則你會(huì)被它所傷。
你察覺(jué)在測(cè)試之后是沒(méi)有整理(tidying)的么促王?
這點(diǎn)非常重要:必須在測(cè)試前準(zhǔn)備環(huán)境掩完,而非測(cè)試之后。你無(wú)法確保測(cè)試將能夠清理一切硼砰。應(yīng)用程序可能因?yàn)槟硞€(gè)錯(cuò)誤且蓬、網(wǎng)絡(luò)連接失敗而退出,或者應(yīng)用程序可能崩潰(例如题翰,由于內(nèi)存不足異常)恶阴。該測(cè)試如何終止并不重要诈胜,真正重要的是該測(cè)試運(yùn)行在為每個(gè)測(cè)試所準(zhǔn)備的相同環(huán)境中。
我曾犯過(guò)這個(gè)錯(cuò)誤:有大量的集成測(cè)試冯事,它們會(huì)在每次測(cè)試后清理所有更改焦匈。許多程序員正使用調(diào)試器來(lái)運(yùn)行這些測(cè)試,并且當(dāng)發(fā)現(xiàn)bug后會(huì)在中間停止測(cè)試昵仅。任何在該測(cè)試之后運(yùn)行的測(cè)試會(huì)得到不可預(yù)知的且隨機(jī)的結(jié)果缓熟。因?yàn)樗\(yùn)行在已被前一測(cè)試所改變的環(huán)境中,而且沒(méi)有為其清理整個(gè)環(huán)境摔笤。
準(zhǔn)備了數(shù)據(jù)庫(kù)卻不對(duì)其檢查就進(jìn)行測(cè)試
在前面的部分中我提到許多有關(guān)準(zhǔn)備數(shù)據(jù)庫(kù)的內(nèi)容够滑。我還想補(bǔ)充一點(diǎn)。準(zhǔn)備數(shù)據(jù)庫(kù)是不夠的吕世。當(dāng)你通過(guò)清理某些表彰触、加載配件等等準(zhǔn)備好數(shù)據(jù)庫(kù)時(shí)……還剩下一件事情要做。
在準(zhǔn)備完畢后命辖,要檢查數(shù)據(jù)庫(kù)狀態(tài)况毅。
你真正需要確保的是你已將一切準(zhǔn)備妥當(dāng)。當(dāng)出現(xiàn)由于bug所導(dǎo)致的某些數(shù)據(jù)留下來(lái)且未能清理時(shí)尔艇,這些工作就可以節(jié)省你的時(shí)間尔许。
這應(yīng)該是測(cè)試前數(shù)據(jù)庫(kù)準(zhǔn)備的一部分。
不測(cè)試創(chuàng)建腳本
每個(gè)應(yīng)用程序都需要某種形式的安裝過(guò)程终娃。而對(duì)于你部署數(shù)據(jù)庫(kù)而言母债,永遠(yuǎn)是第一次。
程序員往往會(huì)通過(guò)手工執(zhí)行某些臨時(shí)的數(shù)據(jù)定義語(yǔ)言(DDL尝抖,data definition language)查詢來(lái)改變數(shù)據(jù)庫(kù)。他們稍后并沒(méi)有把那些語(yǔ)句寫下來(lái)迅皇,或是忘記了所做的改動(dòng)昧辽。他們沒(méi)有更新安裝腳本。大多數(shù)團(tuán)隊(duì)不使用有版本控制的腳本(例如登颓,Ruby on Rails中的Migrations搅荞、或者是Java世界中的Liquibase)。
對(duì)測(cè)試而言最好的方式是框咙,在運(yùn)行測(cè)試套件前重新創(chuàng)建數(shù)據(jù)庫(kù)咕痛。你不必在每個(gè)測(cè)試開(kāi)始前都那么做。只在運(yùn)行所有測(cè)試前運(yùn)行一次就行了喇嘱。
是的茉贡,寧可事先謹(jǐn)慎有余,不要事后追悔莫及者铜。
不測(cè)試外鍵
外鍵是提供數(shù)據(jù)庫(kù)一致性的基本途徑之一腔丧。在良好的關(guān)系數(shù)據(jù)庫(kù)schema中放椰,你應(yīng)該擁有各種鍵。如果你沒(méi)有的話愉粤,那么這可能表明你有一個(gè)真正的大問(wèn)題砾医。然而,這取決于數(shù)據(jù)模型衣厘,但是通常缺乏外鍵是種非常糟糕設(shè)計(jì)的味道如蚜。
測(cè)試外鍵很簡(jiǎn)單。只需在事先沒(méi)有在引用表中添加適當(dāng)?shù)男械那闆r下影暴,為某個(gè)表添加一些行错邦。你應(yīng)該得到一個(gè)錯(cuò)誤。然后坤检,你應(yīng)該從引用表中刪除行兴猩,你可能得到錯(cuò)誤,或沒(méi)有錯(cuò)誤(這取決于該鍵的定義)早歇。無(wú)論如何倾芝,你都應(yīng)該檢查一下預(yù)期行為。
不測(cè)試默認(rèn)值
在良好的數(shù)據(jù)庫(kù)設(shè)計(jì)中箭跳,你應(yīng)該定義一些合理的默認(rèn)值晨另。通常這些默認(rèn)值可能是空(null)。即便這些空也應(yīng)該進(jìn)行測(cè)試谱姓。你不能假設(shè)借尿,只有你的應(yīng)用程序?qū)⒏淖兇藬?shù)據(jù)庫(kù)中的數(shù)據(jù)。\ 兩個(gè)問(wèn)題:
- 如果某人想創(chuàng)建一個(gè)快速修復(fù)并使用臨時(shí)SQL查詢更新某些數(shù)據(jù)屉来,該怎么辦路翻?
- 如果有一天某人啟動(dòng)另一應(yīng)用程序,它可以改變這些數(shù)據(jù)茄靠,而且新的應(yīng)用程序?qū)⒉皇褂媚愕腛RM映射或你的DAO(數(shù)據(jù)訪問(wèn)對(duì)象)類茂契,又該怎么辦?
如果你擁有愚蠢的默認(rèn)值慨绳、或是錯(cuò)誤的默認(rèn)值掉冶,那么你可能會(huì)破會(huì)數(shù)據(jù),而且那可能比一個(gè)簡(jiǎn)單的應(yīng)用程序bug更糟糕脐雪。
不測(cè)試約束
在數(shù)據(jù)庫(kù)中還有更多約束厌小,不僅只有主鍵和外鍵約束。你可能擁有一些唯一的(unique)或不為空的列战秋。你可能約束某列只有很少的值集璧亚。你可能想確保價(jià)格永遠(yuǎn)不會(huì)低于0。
良好的數(shù)據(jù)庫(kù)schema應(yīng)擁有許多約束脂信。你也應(yīng)該測(cè)試它們涨岁。如果你希望你的價(jià)格列只能擁有正值拐袜,當(dāng)你嘗試向其中插入-1美元時(shí)會(huì)發(fā)生些什么?為什么不測(cè)試一下呢梢薪?
你不能假設(shè)只有你的經(jīng)過(guò)良好測(cè)試的應(yīng)用程序?qū)⑹褂媚切?shù)據(jù)蹬铺,而且這些檢查是為你防御這些bug的最后一道防線。為什么不測(cè)試它是否正常工作秉撇?
多個(gè)測(cè)試可以更改同一數(shù)據(jù)庫(kù)
通常甜攀,數(shù)據(jù)庫(kù)測(cè)試會(huì)更改數(shù)據(jù)庫(kù)。你可能同時(shí)運(yùn)行多個(gè)測(cè)試琐馆,但是你必須確保那些測(cè)試彼此之間沒(méi)有影響规阀。你必須確保,如果某個(gè)測(cè)試將一些內(nèi)容寫入數(shù)據(jù)庫(kù)瘦麸,而另一測(cè)試將不會(huì)讀到谁撼。
通常,很容易搞得一塌糊涂滋饲,因此我小小的忠告是:避免在同一時(shí)間運(yùn)行多個(gè)測(cè)試厉碟。這也意味著,你不應(yīng)該在多臺(tái)機(jī)器運(yùn)行相同的測(cè)試套件屠缭。
當(dāng)你有許多想運(yùn)行測(cè)試的開(kāi)發(fā)者時(shí)箍鼓,他們每個(gè)人應(yīng)該擁有可用于編寫測(cè)試的單獨(dú)的數(shù)據(jù)庫(kù)。如果你擁有某種形式的只讀數(shù)據(jù)庫(kù)呵曹,沒(méi)關(guān)系款咖,多臺(tái)機(jī)器可以在同一時(shí)間使用這個(gè)數(shù)據(jù)庫(kù)。但是如果你允許所有程序員使用同一數(shù)據(jù)庫(kù)進(jìn)行測(cè)試的情形出現(xiàn)奄喂,那么你可能真的會(huì)得到不可預(yù)知的測(cè)試結(jié)果铐殃。
當(dāng)程序員在某個(gè)測(cè)試中發(fā)現(xiàn)一個(gè)錯(cuò)誤時(shí)會(huì)怎么做?那么跨新,優(yōu)秀的程序員會(huì)盡量修正錯(cuò)誤富腊。如果該測(cè)試失敗僅僅是因?yàn)榱硪怀绦騿T在同一個(gè)數(shù)據(jù)庫(kù)上運(yùn)行他的測(cè)試所導(dǎo)致的話,那么修正此類錯(cuò)誤只是在浪費(fèi)程序員的時(shí)間玻蝌。
沒(méi)有大紅按鈕
優(yōu)秀的程序員是懶惰的。如果你命令優(yōu)秀的程序員每次都重復(fù)同樣的任務(wù)词疼,他們會(huì)越來(lái)越沮喪。優(yōu)秀的程序員會(huì)自動(dòng)化可重復(fù)的事情。
在每個(gè)項(xiàng)目中蝌蹂,你必須在測(cè)試環(huán)境中部署某些東西噩凹。做這些會(huì)花去多少時(shí)間?你真的想為了重新部署應(yīng)用程序和加載數(shù)據(jù)庫(kù)一直浪費(fèi)你的程序員時(shí)間么舵盈?
這就是為什么每個(gè)項(xiàng)目都應(yīng)該有個(gè)大紅按鈕的原因陋率。某位程序員可以按下此按鈕球化,然后沖杯咖啡,回去工作瓦糟,并且?guī)追昼姾蟮弥拇蠹t色按鈕完成的工作筒愚,一切準(zhǔn)備就緒。
大紅按鈕真的會(huì)為你節(jié)省很多時(shí)間菩浙。你會(huì)說(shuō)自動(dòng)化所有工作實(shí)在太緩慢巢掺。然而,事實(shí)并非如此劲蜻。恰恰相反陆淀,就像說(shuō)測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD,Test-Driven Development)很慢一樣先嬉。在最初的時(shí)候比較慢轧苫,但隨著項(xiàng)目變得更加復(fù)雜,由于存在測(cè)試或按鈕疫蔓,會(huì)為你節(jié)省更多的時(shí)間含懊。各種各樣的大紅按鈕——你可以用它們部署應(yīng)用程序、運(yùn)行測(cè)試鳄袍,以及類似的后方支援绢要。
有時(shí)會(huì)使用Jenkins(又名Hudson)來(lái)做這些。是的拗小,對(duì)于此類大紅按鈕而言這是一款偉大的軟件重罪。唯一的事情是,每位程序員應(yīng)該有其自己的一組工作以便在其自己的環(huán)境中部署所有的內(nèi)容哀九,在其自己的環(huán)境中他(或她剿配,當(dāng)然)可以自由發(fā)揮,而不會(huì)影響他人阅束,同樣也不會(huì)受到他人的影響呼胚。
工具
有許多數(shù)據(jù)庫(kù)測(cè)試工具。為了測(cè)試整個(gè)schema息裸,你可以編寫簡(jiǎn)單的集成測(cè)試蝇更。對(duì)于PostgreSQL有pgTAP,使用TAP插件它可以與Jenkins集成到一起呼盆,因此Jenkins可以擁有一項(xiàng)用于測(cè)試數(shù)據(jù)庫(kù)(包括生產(chǎn)數(shù)據(jù)庫(kù))是否正常的工作年扩。