單元測試資料庫程式碼Unit testing database code

原作者:Richard Dallaway,May 2001

翻譯:Areca Chen 2002/1/29

本文是我在資料庫功能單元測試的筆記。以Java為範例,但我想這些觀念可以應用在不同的程式環境。我也還持續找尋更好的解決方式。

我想解決的問題是:你有一個SQL資料庫,一些儲存程序(stored procedures),及在應用程式與資料庫之間中間層的程式碼。你如何把測試放在正確的地方以確認你的程式碼正確的從資料庫讀寫資料?

原版本: April/May 2001. 本版本: $Revision: 1.2 $ $Date: 2001/07/31 10:41:24 $

更新日期:

 


為什麼要擔心Why bother?

我猜測資料庫應用程式開發應該像這樣:開設資料庫、撰寫程式碼存取資料庫、執行程式碼、執行SELEST看看資料庫所顯示的紀錄。他們過去這樣做?很好,那麼我們已完成了。

『視覺化審視(visual inspection)』的問題是:你不是常常執行,你也不是隨時檢查所有的東西。唯一可能的是當你變動系統;可能是幾個月之後;你破壞了一些東西而某些資料遺漏了。身為程式設計師你可能不會花太多時間檢查資料本身,因此可能不久之後這個錯誤便成為表面化。我曾在一個網頁專案中工作,在一個登錄畫面中有一個指令(mandatory)欄位在最主要的一年未被新增到資料庫中。雖然市場上抗議說他們需要這個資訊,但是問題並未明確指出因為資料從未被看到(但別讓我在那上面開始)。

自動化測試--輕鬆的測試可以不斷的測試而且大量的測試--降低你的資料遺漏的機會。我發現自動化測試可以讓我晚上睡的更好。(測試總是有著正面的形象:測試是如何使用程式碼的好的範例,測試就像一種文件,當你需要變動程式碼時可以讓別人免於擔心其程式碼受遭殃,測試降低除蟲的時間。)

我們討論的是哪類測試What kinds of tests are we talking about?

以一個簡單的使用者資料庫為例,其中有一個欄位放置電子信箱及一個旗標;這個旗標代表此電子信箱是否回復。你的資料庫層可能包含的方法有:insert、update、delete及find。

insert方法呼叫一個儲存程序將電子信箱寫入資料庫欄位中。簡單的程式碼可能看起來像下面:

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());  // 一般而言這個是一個布林值 
    ps.executeUpdate();
    ps.close();
  }
  ...
}

這種的測試碼我目前想到的應該看起來像下面:

public class TestUserDatabase extends TestCase
{
  ...
  public void testInsert()
  {
    //新增一個測試使用者
    User user = new User("some@email.address");
    UserDatabase database = new UserDatabase();
    database.insert(user);

    // 確認資料已存入
    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());
  }
  ...
}

......你應該還有更多的測試(請小心某些測試,如日期的測試)

assertTrue()及assertEquals()方法測試條件式是否為真,而如果測試失敗會顯示一個診斷訊息。概念上此測試是透過一個測試框架自動執行,並 明確的顯示失敗或成功的旗標。那是以JUnit(參閱:資源)為基礎,Junit是Java的測試框架。這個框架可以應用於其他的語言,包括 C, C++, Perl, Python, .NET (所有語言), PL/SQL, Eiffel, Delphi, VB... (參閱:資源).

接下來的問題是:我們已經有了測試,但我們如何管理這些資料庫中的測試資料而不至於污染真實的資料?

無法達到目的的方式Approaches that don't work

在我開始之前,我要指出的是我期望你有一個開發資料庫。你將不要在已是產品的資料庫中做我於此提及的任何事。

我嘗試的第一種方式是手動新增一些測試資料到已是產品的資料庫中。這些資料是已知值的,如『testuser01@test.testing』。如果你要測試搜尋功能,你必須知道這些資料已經在那裡了,如,5個『@test.testing』使用者在資料庫 中。

如上述範例新增測試記錄;測試本身必須維護資料庫狀態。換言之,測試必須在測試完成後確認清除測試資料,並小心不要刪除真正的資料,以便資料庫在測試完成後還原成它原來的狀態。

這種方式有下面幾個原因困擾著我:

 

我嘗試了一些修正:增加一個『is_test』欄位到記錄中,以便作為測試紀錄的旗標。如此可以避免『特定值』的問題。此種方式不利的地方是你的測試需要操作的只 有在is_test欄位標示為真的紀錄,而你已是產品的程式碼操作的記錄是is_test標示為偽的紀錄。如果在這個層次有所差異,你測試的不是相同的程式碼。

你需要四個資料庫You need four databases

有些其他的想法:好的測試是自我滿足並建立所有它需要的資料。如果你在測試之前讓資料庫是位於已知的狀態測試可以是簡單的。這麼做的一種方式是使用一個分開的單元測試資料庫;並讓這個資料庫在測試案例中控制:測試案例在開始測試之前清除資料庫。

在程式碼中,你可以使用一個dbSetUP()方法達成目的,這個方法看起來像這樣:

  ...
  public void dbSetUp()
  {
    // 把資料庫控制在已知的狀態下: 
    //(於此使用儲存程序是比較好的方式) 
    helper.exec("DELETE FROM SomeSideTable");
    helper.exec("DELETE FROM User");

    //加入一些通常使用的測試案例: 
    ...
  }

任何資料庫測試在做任何事之前先將呼叫dbSetUP(),這個方法將資料庫控制在已知的狀態下(多數是清空),這種方式有下列的優點:

 

這種方式的缺點是你需要不只一個資料庫--但記住--如果需要它們可以放在一個伺服器中執行。我測試的方式現在需要4個資料庫(嗯,兩個成一對。):

  1. 已是產品的資料庫(production database),實際的資料。此資料庫中無測試。
  2. 你的區域性開發資料庫(local development database),在此資料庫中包含大部分的測試。
  3. 可移植的開發資料庫(populated development database),大部分屬於所有開發者共享的;以便於你的應用系統執行並使用實際的資料執行以便看看執行結果;而不是你測試資料庫中的資料。你可能不是非常需要這個資料庫,但可以測試在大量資料下你的應用系統的狀況(例如從已是產品的資料庫中複製資料。)
  4. 部署資料庫(deployment database)或整合資料庫(integration database),這個資料庫在部署之前執行測試以確認所有的區域性資料庫的改變已完成。如果你是單獨作業可能不需要如此,但你必須確認任何資料庫結構或儲存程序的變動已經在已是產品 的資料庫中完成。

 

使用多個資料庫你必須確保所有的資料庫結構同步:如果你在你的測試機器中變動一個表格(table)定義或儲存程序,你必須記得也改變其他的資料庫,部署資料庫應該扮演資料庫改變通知者的角色。同時我發現如果 變動的通知是自動以email的方式告知所有開發者;那麼資源控制系統在此有所幫助。CVS(參閱:資源

測試相對正確的資料庫Test against the right database

在此環境中你必須確認你連接的資料庫是正確的,執行測試於已是產品的資料庫會刪除你所有的資料。這是很恐怖的。

有一種方式可以保護你的資料庫。例如,通常的方式是將資料庫連接設定在一個初始檔案中;在使用這個檔案決定哪一個檔案是測試資料庫。你可能使用一個初始檔案作為測試之用(指向區域性的資料庫),而其他的初始檔案為產品資料庫所用(指向一個實際的資料庫。)

在Java,一個初始檔案看起來像這樣:

  myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase

這是一個連接資料庫的字串。你也可以加入第二個連接資料庫字串以界定測試資料庫:

  myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase
  myapp.db.testurl=jdbc:mysql://127.0.0.1/my_test_database

在測試程式碼中你可以增加檢查以確認你將只是連接到一個測試資料庫:

  public void dbSetUp()
  {
     String test_db = InitProperties.get("myapp.db.testurl");
     String db = InitProperties.get("myapp.db.url");

     if (test_db == null)
       abort("未設定資料庫 ");

     if (test_db.equals(db))
     {
        //全部正常:我們連接的資料庫是與設定為『測試用』的相同。 

     }
     else
     {
        abort("將不會執行測試於一個非測試用的資料庫。");
     }
  }

另一個技巧:如果你有一個區域性資料庫,測試可以檢查要執行資料庫的IP或主機名稱,如果不是區域名稱/127.0.0.1很可能你要執行的資料庫是實際的資料庫。

結論Conclusions

在此筆記當中我嘗試說明的是:

 

這個問題還有他的解決方式。在模擬物件(mock objects(參閱:資源))我尚未有充分的信心。據我所知的模擬物件,你模擬(simulate)系統的一個層(在這個案例中是RDBMS),因此你的模擬資料庫總是傳回以所期望的。我喜歡聽到這個聲音:它鼓勵你安排(layer)你的測試,可能是經由使用一個測試的SQL層組(SQL-level set),然後使用一個Java層組(Java-level set)於模擬ResultSet物件上執行工作。

我考慮的只是一些動作可以導致於一兩個表格之中的變動,而於此觀點模擬物件/模擬可能變成痛苦的維護及實作。同時;當然,我將需要找尋一個好的方法測試資料庫的SQL層:記住,我要確認資料是真正適當的存在於資料庫之中。

註釋Comments

我一直在找尋更好的資料庫程式碼測試方法。如果你有任何想法,或任何評論請email給我(dbtest@dallaway.com)或使用討論區

測試日期的相關說明A note on testing dates

如果你儲存日期資訊;你可能要去確認你所儲存的是正確的日期。你要認知到一些議題。

首先,問問你自己是誰建立日期?如果是你的程式碼,那還好;因為當你檢視資料庫時你能夠比對你建立的及你取回的日期。如果是資料庫建立的日期;可能是預設值,那麼你可能會有一些問題。例如:你是否確認你的程式碼的時間範圍(timezone)與資料庫的時間範圍是一致的?當資料庫在GMT/UTC中執行時這是常常聽到的。你是否確認你執行的機器上的時鐘是與設定在資料庫上的時間是一致的?若否,當你比對時間時你將有一些時間差的錯誤(margin of error)。

如果你存在這種情況時有些事情是你可以做的:

 

資源Resources

在Yahoo!中的討論區偶而有討論到資料庫的單元測試,我在其中找到一些相當有用的討論。

原文最後更新日期:Wednesday, 26-Sep-2001 09:19:05 BST

譯文最後更新日期:2008/08/23

本文獲授權同意翻譯函:

I'm flattered that you'd take the time to translate the article.  You
have my permission to do so.

All I'd ask in return is:

1) Let me know when you've finished so I can also link to your translation

2) If possible, supply me with a small graphic (JPG, GIF or PNG) with
the phrase "View this article in traditional Chinese" in traditional
Chinese -- or whatever phrase you think is appropriate -- with black
characters on a white background.   This will allow me to provide a link
for those who don't have traditional Chinese support in there browser.

Regards
Richard


>
> Dear Richard,
> I've read the articles " Unit testing database code
> <http://www.dallaway.com/acad/dbunit.html>"on the web site and like it
> very much.
> It will be great if I could translate it into traditional Chinese and
> publish on our own site http://www.dotspace.idv.tw
> <http://www.dotspace.idv.tw/>) to help those who want to explore unit
> test more. I will, of course, declare the source and "hyperlink" those
> to your original documents. I'll be very grateful if you could give your
> authorization for me to do this.
> Our web site contents about software engineering topics like XP, UML,
> Rup, Design Patterns, Framework, those are all in traditional Chinese.
>         Regards,
> Areca Chen

> 2002/2/8