2009-07-29 83 views
6

我試圖在個人PHP項目中進行單元測試,就像一個好的小程序員一樣,我想正確地做到這一點。從我聽到的你應該測試的只是方法的公共接口,但我想知道這是否仍然適用於下面。單元測試應該走多遠?

我有一種方法可以在用戶忘記密碼的情況下生成密碼重置令牌。該方法返回以下兩種情況之一:如果一切正常,則返回nothing(null),或者返回錯誤代碼,表示具有指定用戶名的用戶不存在。

如果我只測試公共接口,如果用戶名是有效的,如何確保密碼重置標記進入數據庫,並且如果用戶名無效則不會進入數據庫?我應該在我的測試中進行查詢以驗證這一點嗎?或者我應該認爲我的邏輯是正確的?

現在這種方法非常簡單,這不是什麼大問題 - 問題是這種情況也適用於其他許多方法。你在以數據庫爲中心的單元測試中做什麼?如果需要的話

代碼,以供參考:

public function generatePasswordReset($username) 
{ 
    $this->sql='SELECT id 
       FROM users 
       WHERE username = :username'; 

    $this->addParam(':username', $username); 
    $user=$this->query()->fetch(); 

    if (!$user) 
     return self::$E_USER_DOESNT_EXIST; 
    else 
    { 
     $code=md5(uniqid()); 
     $this->addParams(array(':uid'  => $user['id'], 
           ':code'  => $code, 
           ':duration' => 24 //in hours, how long reset is valid 
          )); 

     //generate new code, delete old one if present 
     $this->sql ='DELETE FROM password_resets WHERE user_id=:uid;'; 
     $this->sql.="INSERT INTO password_resets (user_id, code, expires) 
        VALUES  (:uid, :code, now() + interval ':duration hours')"; 

     $this->execute(); 
    } 
} 
+1

對於我來說,至少對單元測試來說,偉大之處在於它向您展示了您需要重構的地方。它還有助於突出顯示依賴關係。我建議你的`SELECT`和你的`DELETE + INSERT`應該被重構到它們自己的方法中,密碼的生成應該在它自己的方法中 – 2009-07-29 13:14:02

+0

@pcampbell - 你的評論應該是一個答案 – 2009-07-29 13:17:36

回答

6

對於我來說至少,單元測試的偉大之處在於它向您展示了需要重構的地方。使用上面的示例代碼,你基本上有四件事在一個方法發生:

//1. get the user from the DB 
//2. in a big else, check if user is null 
//3. create a array containing the userID, a code, and expiry 
//4. delete any existing password resets 
//5. create a new password reset 

單元測試也是偉大的,因爲它有助於突出的依賴。如上所示,此方法依賴於數據庫,而不是實現接口的對象。該方法與超出範圍的系統進行交互,並且只能通過集成測試進行測試,而不是單元測試。單元測試是爲了確保工作單元的工作/正確性。考慮Single Responsibility Principle:「Do one thing」。它適用於方法和類。

我建議你generatePasswordReset方法應該是重構爲:

  • 得到一個預先定義的現有用戶對象/ ID。在這種方法之外做所有那些健康檢查。做一件事。
  • 將密碼重置代碼放入其自己的方法中。這將是一個單獨的工作單元,可以獨立於SELECT,DELETEINSERT進行測試。
  • 創建一個可以稱爲OverwriteExistingPwdChangeRequests()的新方法,它可以處理DELETE + INSERT。
0

一般一個可能「模擬」要調用的對象,驗證其接收的預期請求。

在這種情況下,我不確定這是多麼有幫助,你amost最終編寫了同樣的邏輯兩次......我們認爲我們發送了「從密碼刪除」等哦看看我們沒有!

嗯,我們實際檢查了什麼。如果字符串形成嚴重,我們就不會知道!

這可能違背單元測試法的規定,但我會通過對數據庫進行單獨的查詢來測試這些副作用。

1

你可以把它分解得更多一點,那個函數做的很多,這使得測試有點棘手,不是不可能,而是棘手。另一方面,如果你抽出了一些額外的小函數(getUserByUsername,deletePasswordByUserID,addPasswordByUserId等等),那麼你可以輕鬆地測試一下這些函數,並知道它們的工作原理,所以你不必再次測試它們。確保它們是合理的,這樣你就不用擔心它們會進一步上鍊了,那麼對於這個函數,你所需要做的就是拋出一個不存在的用戶,並確保它返回一個USER_DOESNT_EXIST錯誤那麼一個用戶確實存在的地方(這是你測試DB的地方),內部工作已經在其他地方得到了實施(希望)

0

測試公共接口是必要的,但還不夠。需要多少測試,我只能給出我自己的意見,測試一切,字面上,你應該有一個測試,驗證每一行的代碼已由測試套件執行。 (我只說'每一行',因爲我在考慮C和gcov,而gcov提供了行級粒度,如果你有一個更高分辨率的工具,可以使用它。)如果你可以添加一段代碼給你代碼庫而不添加測試,測試套件應該失敗。

3

此函數更難以進行單元測試的原因是因爲數據庫更新是函數的副作用(即沒有明確的返回值供您測試)。

處理這種遠程對象狀態更新的一種方法是創建一個模擬對象,它提供與數據庫相同的接口(即從代碼的角度看它看起來相同)。然後在測試中,您可以檢查模擬對象中的狀態更改並確認您已收到您應該的內容。

+0

「數據庫的多少」 「你需要執行以便能夠檢查這些狀態變化嗎?解析SQL?維護一些數據模仿表?我的觀點是,使用真正的數據庫最終會減少工作量。 – djna 2009-07-29 13:31:17

0

數據庫是全局變量。 全局變量是使用它們的每個單元的公共接口。因此,測試用例不僅需要改變函數參數的輸入,還要改變數據庫的輸入。

0

如果你的單元測試有副作用(如更改數據庫),那麼他們已經成爲集成測試。集成測試本身沒有任何問題;任何自動化測試都有利於您的產品質量。但是集成測試的維護成本更高,因爲它們更復雜,更容易中斷。

因此,最小化只能用副作用測試的代碼。將SQL查詢隔離並隱藏在不包含任何業務邏輯的獨立MyDatabase類中。將此對象的實例傳遞給您的業務邏輯代碼。

然後當您單元測試您的業務邏輯時,您可以將MyDatabase對象替換爲未連接到真實數據庫的模擬實例,以及可用於驗證您的業務邏輯代碼是否正確使用數據庫的模擬實例。

查看文檔SimpleTest(一個php模擬框架)爲例。

1

單元測試的目的是驗證單元是否工作。如果你想知道一個單位是否有效,寫一個測試。就這麼簡單。選擇是否編寫單元測試不應該基於某些圖表或經驗法則。作爲一名專業人員,您有責任提供工作代碼,除非您進行測試,否則您無法確定它是否正常工作。

現在,這並不意味着您爲每一行代碼編寫測試。這也不一定意味着你爲每一個函數編寫單元測試。決定是否測試某個特定的工作單元可能會導致風險。你有多願意冒這樣一段未經測試的代碼被部署?

如果你問自己「我怎麼知道這個功能是否有效」,答案是「你不知道,除非你有可重複的測試證明它有效」。