首先,只要你的測試用例是不是單元測試,這就是所謂的整合測試,因爲它依賴於環境中可用的MySQL服務器上。
然後,我們將進行集成測試。在proper DB testing with PHPUnit複雜性讓事情變得足夠簡單,不鑽研,這裏的例子測試用例類,考慮到與可用性寫着:
tests.php
<?php
require_once(__DIR__.'/code.php');
class BruteForceTests extends PHPUnit_Framework_TestCase
{
/** @test */
public function NoLoginAttemptsNoBruteforce()
{
// Given empty dataset any random time will do
$any_random_time = date('H:i');
$this->assertFalse(
$this->isUserTriedToBruteForce($any_random_time)
);
}
/** @test */
public function DoNotDetectBruteforceIfLessThanFiveLoginAttemptsInLastTwoHours()
{
$this->userLogged('5:34');
$this->userLogged('4:05');
$this->assertFalse(
$this->isUserTriedToBruteForce('6:00')
);
}
/** @test */
public function DetectBruteforceIfMoreThanFiveLoginAttemptsInLastTwoHours()
{
$this->userLogged('4:36');
$this->userLogged('4:23');
$this->userLogged('4:00');
$this->userLogged('3:40');
$this->userLogged('3:15');
$this->userLogged('3:01'); // ping! 6th login, just in time
$this->assertTrue(
$this->isUserTriedToBruteForce('5:00')
);
}
//==================================================================== SETUP
/** @var PDO */
private $connection;
/** @var PDOStatement */
private $inserter;
const DBNAME = 'test';
const DBUSER = 'tester';
const DBPASS = 'secret';
const DBHOST = 'localhost';
public function setUp()
{
$this->connection = new PDO(
sprintf('mysql:host=%s;dbname=%s', self::DBHOST, self::DBNAME),
self::DBUSER,
self::DBPASS
);
$this->assertInstanceOf('PDO', $this->connection);
// Cleaning after possible previous launch
$this->connection->exec('delete from login_attempts');
// Caching the insert statement for perfomance
$this->inserter = $this->connection->prepare(
'insert into login_attempts (`user_id`, `time`) values(:user_id, :timestamp)'
);
$this->assertInstanceOf('PDOStatement', $this->inserter);
}
//================================================================= FIXTURES
// User ID of user we care about
const USER_UNDER_TEST = 1;
// User ID of user who is just the noise in the DB, and should be skipped by tests
const SOME_OTHER_USER = 2;
/**
* Use this method to record login attempts of the user we care about
*
* @param string $datetime Any date & time definition which `strtotime()` understands.
*/
private function userLogged($datetime)
{
$this->logUserLogin(self::USER_UNDER_TEST, $datetime);
}
/**
* Use this method to record login attempts of the user we do not care about,
* to provide fuzziness to our tests
*
* @param string $datetime Any date & time definition which `strtotime()` understands.
*/
private function anotherUserLogged($datetime)
{
$this->logUserLogin(self::SOME_OTHER_USER, $datetime);
}
/**
* @param int $userid
* @param string $datetime Human-readable representation of login time (and possibly date)
*/
private function logUserLogin($userid, $datetime)
{
$mysql_timestamp = date('Y-m-d H:i:s', strtotime($datetime));
$this->inserter->execute(
array(
':user_id' => $userid,
':timestamp' => $mysql_timestamp
)
);
$this->inserter->closeCursor();
}
//=================================================================== HELPERS
/**
* Helper to quickly imitate calling of our function under test
* with the ID of user we care about, clean connection of correct type and provided testing datetime.
* You can call this helper with the human-readable datetime value, although function under test
* expects the integer timestamp as an origin date.
*
* @param string $datetime Any human-readable datetime value
* @return bool The value of called function under test.
*/
private function isUserTriedToBruteForce($datetime)
{
$connection = $this->tryGetMysqliConnection();
$timestamp = strtotime($datetime);
return wasTryingToBruteForce(self::USER_UNDER_TEST, $connection, $timestamp);
}
private function tryGetMysqliConnection()
{
$connection = new mysqli(self::DBHOST, self::DBUSER, self::DBPASS, self::DBNAME);
$this->assertSame(0, $connection->connect_errno);
$this->assertEquals("", $connection->connect_error);
return $connection;
}
}
該測試套件是自包含的,並已三個測試用例:當沒有登錄嘗試記錄時,用於在檢查時間的兩個小時內有六個登錄嘗試記錄,並且在同一時間範圍內只有兩個登錄嘗試記錄時。
這是測試套件不足,例如,您需要測試bruteforce的檢查是否真正適用於我們感興趣的用戶,並忽略其他用戶的登錄嘗試。另一個例子是你的函數應該在檢查時間結束時間的兩個小時間隔內真正選擇記錄,而不是在檢查時間減去兩小時後(如現在這樣)存儲的所有記錄。您可以自己編寫所有剩餘的測試。
這個測試套件與PDO
連接到DB,它絕對優於接口mysqli
,但是對於被測功能的需求,它會創建相應的連接對象。
一個很重要的應當注意到:你的功能,因爲它是因爲不可控制的庫函數這裏靜態依賴的不可測:
// Get timestamp of current time
$now = time();
檢查的時間應提取功能函數參數可以通過自動方式進行測試,如下所示:
function wasTryingToBruteForce($user_id, $connection, $now)
{
if (!$now)
$now = time();
//... rest of code ...
}
正如您所看到的,我已將您的函數更名爲更清晰的名稱。
除此之外,我想你應該非常小心working with datetime values in between MySQL and PHP,並且也永遠不會通過連接字符串構造SQL查詢,而是使用參數綁定來代替。所以,你最初的代碼稍微清理版本如下(請注意,測試套件需要它的第一行):
code.php
<?php
/**
* Checks whether user was trying to bruteforce the login.
* Bruteforce is defined as 6 or more login attempts in last 2 hours from $now.
* Default for $now is current time.
*
* @param int $user_id ID of user in the DB
* @param mysqli $connection Result of calling `new mysqli`
* @param timestamp $now Base timestamp to count two hours from
* @return bool Whether the $user_id tried to bruteforce login or not.
*/
function wasTryingToBruteForce($user_id, $connection, $now)
{
if (!$now)
$now = time();
$two_hours_ago = $now - (2 * 60 * 60);
$since = date('Y-m-d H:i:s', $two_hours_ago); // Checking records of login attempts for last 2 hours
$stmt = $connection->prepare("SELECT time FROM login_attempts WHERE user_id = ? AND time > ?");
if ($stmt) {
$stmt->bind_param('is', $user_id, $since);
// Execute the prepared query.
$stmt->execute();
$stmt->store_result();
// If there has been more than 5 failed logins
if ($stmt->num_rows > 5) {
return true;
} else {
return false;
}
}
}
對於我個人的口味,檢查的這種方法是非常低效的,你可能真的想下面的查詢:
select count(time)
from login_attempts
where
user_id=:user_id
and time between :two_hours_ago and :now
由於這是集成測試,它希望在它的數據庫和FOLL工作訪問MySQL實例虧欠的表中定義:
mysql> describe login_attempts;
+---------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+------------------+------+-----+-------------------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| user_id | int(10) unsigned | YES | | NULL | |
| time | timestamp | NO | | CURRENT_TIMESTAMP | |
+---------+------------------+------+-----+-------------------+----------------+
3 rows in set (0.00 sec)
它給出的功能測試中的運作只是我個人的猜測,但我想你確實有這樣的表。
在運行測試之前,您必須在tests.php
文件中的「SETUP」部分中配置DB*
常量。
隨着成功,我的意思是管理設置測試。如果你要測試你會怎麼做?我一直在尋找該頁面,但無法將其應用於我的代碼。 – user2354898
你的問題確實不夠具體,但是當我開始設置單元測試時,我發現進入的障礙很困難,所以我試圖幫助。您應該先閱讀我發佈的鏈接。 – dflash
PHPUnit將測試斷言($ this-> assert ...),所以在我的示例中,我正在測試checkbrute的返回值是否爲true。如果測試成功,則測試失敗。有很多事情要說,這取決於你想要測試什麼,這是不明確的。 – dflash